From e3411d0b913656facae0d16eada84bbb30bb7a3f Mon Sep 17 00:00:00 2001
From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
Date: Fri, 14 Feb 2025 05:54:48 +0700
Subject: [PATCH 1/2] feat(quiz): allow revealing correct answers on success
---
src/quiz-question/answer.tsx | 4 +-
src/quiz-question/quiz-question.tsx | 2 +-
src/quiz/quiz.stories.tsx | 224 ++++++++++++++++++++++++++++
src/quiz/use-quiz.test.ts | 99 ++++++++++++
src/quiz/use-quiz.ts | 32 ++--
5 files changed, 349 insertions(+), 12 deletions(-)
diff --git a/src/quiz-question/answer.tsx b/src/quiz-question/answer.tsx
index a783869b..3f7ba12a 100644
--- a/src/quiz-question/answer.tsx
+++ b/src/quiz-question/answer.tsx
@@ -113,9 +113,9 @@ export const Answer = ({
const getRadioWrapperCls = () => {
const cls = [...radioWrapperDefaultClasses];
- if (checked && validation?.state === "correct")
+ if (validation?.state === "correct")
cls.push("border-l-background-success");
- if (checked && validation?.state === "incorrect")
+ if (validation?.state === "incorrect")
cls.push("border-l-background-danger");
return cls.join(" ");
diff --git a/src/quiz-question/quiz-question.tsx b/src/quiz-question/quiz-question.tsx
index 51591c52..e88a18b1 100644
--- a/src/quiz-question/quiz-question.tsx
+++ b/src/quiz-question/quiz-question.tsx
@@ -75,7 +75,7 @@ export const QuizQuestion = ({
feedback={checked && validation && feedback}
checked={checked}
disabled={disabled}
- validation={checked ? validation : undefined}
+ validation={validation}
/>
);
})}
diff --git a/src/quiz/quiz.stories.tsx b/src/quiz/quiz.stories.tsx
index 17e55ec7..406a5617 100644
--- a/src/quiz/quiz.stories.tsx
+++ b/src/quiz/quiz.stories.tsx
@@ -224,6 +224,111 @@ const QuizWithValidationAndAnswerFeedback = () => {
);
};
+const QuizWithCorrectAnswersShownOnSuccess = () => {
+ const initialQuestions: Question[] = [
+ {
+ question: "Lorem ipsum dolor sit amet",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ feedback: (
+ Quaerat in autem sapiente illum. Vel mollitia omnis qui dolorem um
esse eos maiores possimus. Est laborum quam aliquam qui sunt. Ut ea et qui provident voluptatibus. Eius quam odit sint cumque sint. Corporis quia et dicta.
`}
+ getCodeBlockAriaLabel={(codeName) => `${codeName} code example`}
+ />
+ ),
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ feedback:
+ "Recusandae necessitatibus consequatur voluptatem sapiente.",
+ },
+ { label: "Option 3", value: 3, feedback: "Voluptas et et animi quo." },
+ ],
+ correctAnswer: 1,
+ },
+ {
+ question: "Consectetur adipiscing elit",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ feedback: (
+ Quaerat in autem sapiente illum. Vel mollitia omnis qui dolorem um
esse eos maiores possimus. Est laborum quam aliquam qui sunt. Ut ea et qui provident voluptatibus. Eius quam odit sint cumque sint. Corporis quia et dicta.`}
+ getCodeBlockAriaLabel={(codeName) => `${codeName} code example`}
+ />
+ ),
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ feedback:
+ "Recusandae necessitatibus consequatur voluptatem sapiente.",
+ },
+ { label: "Option 3", value: 3, feedback: "Voluptas et et animi quo." },
+ ],
+ correctAnswer: 2,
+ },
+ {
+ question: "Fugit itaque delectus voluptatem alias aliquid",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ feedback: (
+ Quaerat in autem sapiente illum. Vel mollitia omnis qui dolorem um
esse eos maiores possimus. Est laborum quam aliquam qui sunt. Ut ea et qui provident voluptatibus. Eius quam odit sint cumque sint. Corporis quia et dicta.`}
+ getCodeBlockAriaLabel={(codeName) => `${codeName} code example`}
+ />
+ ),
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ feedback:
+ "Recusandae necessitatibus consequatur voluptatem sapiente.",
+ },
+ { label: "Option 3", value: 3, feedback: "Voluptas et et animi quo." },
+ ],
+ correctAnswer: 3,
+ },
+ ];
+
+ const { questions, validateAnswers, correctAnswerCount } = useQuiz({
+ initialQuestions,
+ validationMessages: {
+ correct: "Correct.",
+ incorrect: "Incorrect.",
+ },
+ passingGrade: 50,
+ showCorrectAnswersOnSuccess: true,
+ });
+ const [disabled, setDisabled] = useState(false);
+
+ const handleSubmit = () => {
+ validateAnswers();
+ setDisabled(true);
+ };
+
+ return (
+
+
+ {!!correctAnswerCount && (
+
+ Correct answers: {correctAnswerCount}
+
+ )}
+
+
+
+
+
+ );
+};
+
export const Default: Story = {
render: QuizDefault,
args: {},
@@ -471,4 +576,123 @@ const App = () => {
},
};
+export const WithCorrectAnswersShownOnSuccess: Story = {
+ render: QuizWithCorrectAnswersShownOnSuccess,
+ args: {},
+ parameters: {
+ docs: {
+ source: {
+ code: `
+import { Quiz, useQuiz, Button, Spacer } from '@freecodecamp/ui';
+
+const initialQuestions = [
+ {
+ question: "Lorem ipsum dolor sit amet",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ feedback: (
+ Quaerat in autem sapiente illum. Vel mollitia omnis qui dolorem um
esse eos maiores possimus. Est laborum quam aliquam qui sunt. Ut ea et qui provident voluptatibus. Eius quam odit sint cumque sint. Corporis quia et dicta.\`}
+ getCodeBlockAriaLabel={(codeName) => \`\${codeName} code example\`}
+ />
+ ),
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ feedback:
+ "Recusandae necessitatibus consequatur voluptatem sapiente.",
+ },
+ { label: "Option 3", value: 3, feedback: "Voluptas et et animi quo." },
+ ],
+ correctAnswer: 1,
+ },
+ {
+ question: "Consectetur adipiscing elit",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ feedback: (
+ Quaerat in autem sapiente illum. Vel mollitia omnis qui dolorem um
esse eos maiores possimus. Est laborum quam aliquam qui sunt. Ut ea et qui provident voluptatibus. Eius quam odit sint cumque sint. Corporis quia et dicta.\`}
+ getCodeBlockAriaLabel={(codeName) => \`\${codeName} code example\`}
+ />
+ ),
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ feedback:
+ "Recusandae necessitatibus consequatur voluptatem sapiente.",
+ },
+ { label: "Option 3", value: 3, feedback: "Voluptas et et animi quo." },
+ ],
+ correctAnswer: 2,
+ },
+ {
+ question: "Fugit itaque delectus voluptatem alias aliquid",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ feedback: (
+ Quaerat in autem sapiente illum. Vel mollitia omnis qui dolorem um
esse eos maiores possimus. Est laborum quam aliquam qui sunt. Ut ea et qui provident voluptatibus. Eius quam odit sint cumque sint. Corporis quia et dicta.\`}
+ getCodeBlockAriaLabel={(codeName) => \`\${codeName} code example\`}
+ />
+ ),
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ feedback:
+ "Recusandae necessitatibus consequatur voluptatem sapiente.",
+ },
+ { label: "Option 3", value: 3, feedback: "Voluptas et et animi quo." },
+ ],
+ correctAnswer: 3,
+ },
+];
+
+const App = () => {
+ const { questions, validateAnswers } = useQuiz({
+ initialQuestions,
+ validationMessages: {
+ correct: "Correct.",
+ incorrect: "Incorrect.",
+ },
+ passingGrade: 50,
+ showCorrectAnswersOnSuccess: true
+ });
+
+ const [disabled, setDisabled] = useState(false);
+
+ const handleSubmit = () => {
+ validateAnswers();
+ setDisabled(true);
+ };
+
+ return (
+
+
+ {!!correctAnswerCount && (
+
+ Correct answers: {correctAnswerCount}
+
+ )}
+
+
+
+
+
+ );
+};`,
+ },
+ },
+ },
+};
+
export default story;
diff --git a/src/quiz/use-quiz.test.ts b/src/quiz/use-quiz.test.ts
index 29493213..4282640a 100644
--- a/src/quiz/use-quiz.test.ts
+++ b/src/quiz/use-quiz.test.ts
@@ -197,6 +197,105 @@ describe("useQuiz", () => {
expect(result.current.validated).toBe(true);
});
+ it("should return the questions array with the correct validation status if `showCorrectAnswersOnSuccess` is `true`", () => {
+ const { result } = renderHook(() =>
+ useQuiz({
+ initialQuestions: [
+ {
+ question: "Lorem ipsum dolor sit amet",
+ answers: [
+ { label: "Option 1", value: 1 },
+ { label: "Option 2", value: 2 },
+ { label: "Option 3", value: 3 },
+ ],
+ selectedAnswer: 1,
+ correctAnswer: 1,
+ },
+ {
+ question: "Consectetur adipiscing elit",
+ answers: [
+ { label: "Option 1", value: 1 },
+ { label: "Option 2", value: 2 },
+ { label: "Option 3", value: 3 },
+ ],
+ selectedAnswer: 3,
+ correctAnswer: 2,
+ },
+ ],
+ validationMessages,
+ passingGrade: 50,
+ showCorrectAnswersOnSuccess: true,
+ }),
+ );
+
+ expect(result.current.validated).toBe(false);
+ expect(result.current.correctAnswerCount).toBeUndefined();
+ expect(result.current.grade).toBeUndefined();
+
+ act(() => {
+ result.current.validateAnswers();
+ });
+
+ expect(result.current.questions).toMatchObject([
+ {
+ question: "Lorem ipsum dolor sit amet",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ validation: {
+ message: "Correct",
+ state: "correct",
+ },
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ },
+ {
+ label: "Option 3",
+ value: 3,
+ },
+ ],
+ onChange: expect.any(Function),
+ selectedAnswer: 1,
+ correctAnswer: 1,
+ },
+ {
+ question: "Consectetur adipiscing elit",
+ answers: [
+ {
+ label: "Option 1",
+ value: 1,
+ },
+ {
+ label: "Option 2",
+ value: 2,
+ validation: {
+ message: "Correct",
+ state: "correct",
+ },
+ },
+ {
+ label: "Option 3",
+ value: 3,
+ validation: {
+ message: "Incorrect",
+ state: "incorrect",
+ },
+ },
+ ],
+ onChange: expect.any(Function),
+ selectedAnswer: 3,
+ correctAnswer: 2,
+ },
+ ]);
+
+ expect(result.current.correctAnswerCount).toBe(1);
+ expect(result.current.grade).toBe(50);
+ expect(result.current.validated).toBe(true);
+ });
+
it("should call the `onSuccess` function if the quiz results meet the passing grade", () => {
const onSuccess = jest.fn();
const onFailure = jest.fn();
diff --git a/src/quiz/use-quiz.ts b/src/quiz/use-quiz.ts
index 0347b79b..56b5db74 100644
--- a/src/quiz/use-quiz.ts
+++ b/src/quiz/use-quiz.ts
@@ -21,6 +21,7 @@ interface Props {
passingGrade: number;
onSuccess?: () => void;
onFailure?: () => void;
+ showCorrectAnswersOnSuccess?: boolean;
}
type ValidationData =
@@ -38,6 +39,7 @@ export const useQuiz = ({
onSuccess,
onFailure,
passingGrade,
+ showCorrectAnswersOnSuccess,
}: Props): UseQuizReturnType => {
const [questions, setQuestions] =
useState[]>(initialQuestions);
@@ -61,12 +63,20 @@ export const useQuiz = ({
const validateAnswers = () => {
setQuestions((prevQuestion) => {
+ const correctCount = prevQuestion.filter(
+ ({ selectedAnswer, correctAnswer }) => selectedAnswer === correctAnswer,
+ ).length;
+
+ const grade = parseFloat(
+ ((correctCount / initialQuestions.length) * 100).toFixed(2),
+ );
+
const updatedQuestions: Question[] = prevQuestion.map(
(question) => {
const answersWithValidation = question.answers.map((answer) => {
let validation: QuizQuestionAnswer["validation"];
- // Only pass validation to the selected answer
+ // Pass validation to the selected answer
if (answer.value === question.selectedAnswer) {
validation =
answer.value === question.correctAnswer
@@ -78,6 +88,18 @@ export const useQuiz = ({
state: "incorrect",
message: validationMessages.incorrect,
};
+ } else {
+ // Reveal the correct answer if the results meet the passing grade
+ if (
+ answer.value === question.correctAnswer &&
+ grade >= passingGrade &&
+ showCorrectAnswersOnSuccess
+ ) {
+ validation = {
+ state: "correct",
+ message: validationMessages.correct,
+ };
+ }
}
return { ...answer, validation };
@@ -87,14 +109,6 @@ export const useQuiz = ({
},
);
- const correctCount = updatedQuestions.filter(
- ({ selectedAnswer, correctAnswer }) => selectedAnswer === correctAnswer,
- ).length;
-
- const grade = parseFloat(
- ((correctCount / initialQuestions.length) * 100).toFixed(2),
- );
-
setValidation({
validated: true,
grade,
From abbe143dfcfc1c8b5e5f104bf75611899232f1fa Mon Sep 17 00:00:00 2001
From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:03:57 +0700
Subject: [PATCH 2/2] fix: typecheck
---
src/quiz/quiz.stories.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/quiz/quiz.stories.tsx b/src/quiz/quiz.stories.tsx
index 34733176..706553b9 100644
--- a/src/quiz/quiz.stories.tsx
+++ b/src/quiz/quiz.stories.tsx
@@ -303,7 +303,7 @@ const QuizWithCorrectAnswersShownOnSuccess = () => {
correct: "Correct.",
incorrect: "Incorrect.",
},
- passingGrade: 50,
+ passingPercent: 50,
showCorrectAnswersOnSuccess: true,
});
const [disabled, setDisabled] = useState(false);
@@ -664,7 +664,7 @@ const App = () => {
correct: "Correct.",
incorrect: "Incorrect.",
},
- passingGrade: 50,
+ passingPercent: 50,
showCorrectAnswersOnSuccess: true
});