Skip to content

Commit 5bd9156

Browse files
authored
Merge pull request #5045 from anshul23102/feat/5026-quiz-component
Implement quiz/assessment component for tutorials
2 parents 938a0b3 + 22b993c commit 5bd9156

3 files changed

Lines changed: 616 additions & 0 deletions

File tree

‎docs/examples/QuizExample.mdx‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
id: quiz-example
3+
title: Quiz Component
4+
description: Interactive quiz component for assessments
5+
---
6+
7+
import Quiz from '@site/src/components/Quiz';
8+
9+
# Quiz Component Example
10+
11+
The Quiz component provides an interactive assessment experience with multiple-choice questions, immediate feedback, and keyboard accessibility.
12+
13+
## Features
14+
15+
- Multiple-choice questions with answer reveal
16+
- Explanations after each answer
17+
- Score summary at completion
18+
- Keyboard navigation (arrow keys, Enter)
19+
- Progress tracking
20+
- Responsive design
21+
- Accessibility features (ARIA labels, screen reader support)
22+
23+
## Usage
24+
25+
```jsx
26+
import Quiz from '@site/src/components/Quiz';
27+
28+
const questions = [
29+
{
30+
question: "What is the output of 2 + 2 * 3?",
31+
options: ["8", "12", "10", "7"],
32+
correct: 0,
33+
explanation: "Following order of operations, multiplication is performed before addition: 2 + (2 * 3) = 2 + 6 = 8"
34+
},
35+
{
36+
question: "Which of these is not a primitive type in JavaScript?",
37+
options: ["string", "number", "array", "boolean"],
38+
correct: 2,
39+
explanation: "Arrays are objects in JavaScript, not primitive types. Primitive types include string, number, boolean, undefined, null, and symbol."
40+
}
41+
];
42+
43+
export default function Page() {
44+
return <Quiz questions={questions} />;
45+
}
46+
```
47+
48+
## Question Structure
49+
50+
Each question object must have:
51+
52+
- `question` (string): The question text
53+
- `options` (array of strings): The available answer choices
54+
- `correct` (number): Index of the correct answer (0-based)
55+
- `explanation` (string): Explanation shown after answering
56+
57+
## Keyboard Shortcuts
58+
59+
When a question hasn't been answered:
60+
- **↑ / ↓ Arrow Keys**: Navigate between options
61+
- **Enter**: Select focused option
62+
63+
When a question is answered:
64+
- **← / → Arrow Keys**: Navigate to previous/next question
65+
- **Home**: Jump to first question
66+
- **End**: Jump to last question
67+
68+
## Example Quiz
69+
70+
<Quiz questions={[
71+
{
72+
question: "What does HTML stand for?",
73+
options: [
74+
"Hyper Text Markup Language",
75+
"High Tech Modern Language",
76+
"Home Tool Markup Language",
77+
"Hyperlinks and Text Markup Language"
78+
],
79+
correct: 0,
80+
explanation: "HTML (HyperText Markup Language) is the standard markup language used to create web pages."
81+
},
82+
{
83+
question: "Which CSS property is used to control text spacing?",
84+
options: [
85+
"text-padding",
86+
"letter-spacing",
87+
"word-distance",
88+
"character-gap"
89+
],
90+
correct: 1,
91+
explanation: "The letter-spacing property controls the space between characters in text. For space between words, use word-spacing."
92+
},
93+
{
94+
question: "What is the primary purpose of JavaScript in web development?",
95+
options: [
96+
"Styling web pages",
97+
"Adding interactivity and dynamic behavior",
98+
"Managing server databases",
99+
"Creating HTML structure"
100+
],
101+
correct: 1,
102+
explanation: "JavaScript is used to add interactivity, handle user events, manipulate the DOM, and create dynamic behavior in web applications."
103+
}
104+
]} />
105+
106+
## Customization
107+
108+
The component uses CSS custom properties for theming. You can override these in your custom CSS:
109+
110+
- `--ifm-color-primary`: Primary button and accent color
111+
- `--ifm-color-success`: Correct answer highlight
112+
- `--ifm-color-danger`: Incorrect answer highlight
113+
- `--ifm-color-info`: Selected option highlight

‎src/components/Quiz.jsx‎

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import React, { useState, useCallback, useEffect } from 'react';
2+
import styles from './Quiz.module.css';
3+
4+
export default function Quiz({ questions = [] }) {
5+
const [currentIndex, setCurrentIndex] = useState(0);
6+
const [selectedAnswers, setSelectedAnswers] = useState({});
7+
const [showExplanations, setShowExplanations] = useState({});
8+
const [isComplete, setIsComplete] = useState(false);
9+
const [focusedOption, setFocusedOption] = useState(null);
10+
11+
if (!questions || questions.length === 0) {
12+
return <div className={styles.empty}>No questions available</div>;
13+
}
14+
15+
const currentQuestion = questions[currentIndex];
16+
const currentAnswer = selectedAnswers[currentIndex];
17+
const isAnswered = currentIndex in selectedAnswers;
18+
const correctAnswer = currentQuestion.correct;
19+
const isCorrect = currentAnswer === correctAnswer;
20+
21+
const score = Object.entries(selectedAnswers).filter(([idx, answer]) => {
22+
return questions[parseInt(idx)].correct === answer;
23+
}).length;
24+
25+
const handleSelectAnswer = useCallback((optionIndex) => {
26+
if (!isAnswered) {
27+
setSelectedAnswers(prev => ({
28+
...prev,
29+
[currentIndex]: optionIndex
30+
}));
31+
setShowExplanations(prev => ({
32+
...prev,
33+
[currentIndex]: true
34+
}));
35+
setFocusedOption(null);
36+
}
37+
}, [currentIndex, isAnswered]);
38+
39+
const handleKeyDown = useCallback((e) => {
40+
if (isAnswered) {
41+
if (e.key === 'ArrowRight' && currentIndex < questions.length - 1) {
42+
setCurrentIndex(currentIndex + 1);
43+
setFocusedOption(null);
44+
} else if (e.key === 'ArrowLeft' && currentIndex > 0) {
45+
setCurrentIndex(currentIndex - 1);
46+
setFocusedOption(null);
47+
} else if (e.key === 'End') {
48+
setCurrentIndex(questions.length - 1);
49+
setFocusedOption(null);
50+
} else if (e.key === 'Home') {
51+
setCurrentIndex(0);
52+
setFocusedOption(null);
53+
}
54+
return;
55+
}
56+
57+
if (e.key === 'ArrowDown') {
58+
e.preventDefault();
59+
setFocusedOption(prev => {
60+
const next = (prev === null ? 0 : Math.min(prev + 1, currentQuestion.options.length - 1));
61+
return next;
62+
});
63+
} else if (e.key === 'ArrowUp') {
64+
e.preventDefault();
65+
setFocusedOption(prev => {
66+
const next = (prev === null ? 0 : Math.max(prev - 1, 0));
67+
return prev;
68+
});
69+
} else if (e.key === 'Enter' && focusedOption !== null) {
70+
e.preventDefault();
71+
handleSelectAnswer(focusedOption);
72+
}
73+
}, [currentIndex, isAnswered, focusedOption, questions.length, currentQuestion.options.length, handleSelectAnswer]);
74+
75+
useEffect(() => {
76+
window.addEventListener('keydown', handleKeyDown);
77+
return () => window.removeEventListener('keydown', handleKeyDown);
78+
}, [handleKeyDown]);
79+
80+
const handleNextQuestion = () => {
81+
if (currentIndex < questions.length - 1) {
82+
setCurrentIndex(currentIndex + 1);
83+
setFocusedOption(null);
84+
} else {
85+
setIsComplete(true);
86+
}
87+
};
88+
89+
const handlePreviousQuestion = () => {
90+
if (currentIndex > 0) {
91+
setCurrentIndex(currentIndex - 1);
92+
setFocusedOption(null);
93+
}
94+
};
95+
96+
const handleRestart = () => {
97+
setCurrentIndex(0);
98+
setSelectedAnswers({});
99+
setShowExplanations({});
100+
setIsComplete(false);
101+
setFocusedOption(null);
102+
};
103+
104+
if (isComplete) {
105+
const percentage = Math.round((score / questions.length) * 100);
106+
return (
107+
<div className={styles.container}>
108+
<div className={styles.summary}>
109+
<h2>Quiz Complete!</h2>
110+
<div className={styles.scoreCard}>
111+
<div className={styles.scoreCircle}>{percentage}%</div>
112+
<p className={styles.scoreText}>You got {score} out of {questions.length} correct</p>
113+
</div>
114+
<button className={styles.restartBtn} onClick={handleRestart}>
115+
Restart Quiz
116+
</button>
117+
</div>
118+
</div>
119+
);
120+
}
121+
122+
return (
123+
<div className={styles.container}>
124+
<div className={styles.header}>
125+
<div className={styles.progress}>
126+
Question {currentIndex + 1} of {questions.length}
127+
</div>
128+
<div className={styles.progressBar}>
129+
<div
130+
className={styles.progressFill}
131+
style={{ width: `${((currentIndex + 1) / questions.length) * 100}%` }}
132+
/>
133+
</div>
134+
</div>
135+
136+
<div className={styles.questionCard}>
137+
<h3 className={styles.question}>{currentQuestion.question}</h3>
138+
139+
<div className={styles.optionsContainer}>
140+
{currentQuestion.options.map((option, idx) => {
141+
const isSelected = currentAnswer === idx;
142+
const isOptCorrect = correctAnswer === idx;
143+
const showAsCorrect = isAnswered && isOptCorrect;
144+
const showAsIncorrect = isAnswered && isSelected && !isOptCorrect;
145+
const isFocused = focusedOption === idx && !isAnswered;
146+
147+
return (
148+
<button
149+
key={idx}
150+
className={`${styles.option} ${isSelected ? styles.selected : ''} ${showAsCorrect ? styles.correct : ''} ${showAsIncorrect ? styles.incorrect : ''} ${isFocused ? styles.focused : ''}`}
151+
onClick={() => handleSelectAnswer(idx)}
152+
disabled={isAnswered}
153+
aria-pressed={isSelected}
154+
aria-label={`Option ${String.fromCharCode(65 + idx)}: ${option}`}
155+
>
156+
<span className={styles.optionLabel}>{String.fromCharCode(65 + idx)}</span>
157+
<span className={styles.optionText}>{option}</span>
158+
{isAnswered && isOptCorrect && <span className={styles.checkmark}>✓</span>}
159+
{isAnswered && showAsIncorrect && <span className={styles.xmark}>✗</span>}
160+
</button>
161+
);
162+
})}
163+
</div>
164+
165+
{showExplanations[currentIndex] && (
166+
<div className={`${styles.explanation} ${isCorrect ? styles.successExplanation : styles.errorExplanation}`}>
167+
<strong>{isCorrect ? 'Correct!' : 'Incorrect.'}</strong> {currentQuestion.explanation}
168+
</div>
169+
)}
170+
171+
{isAnswered && (
172+
<div className={styles.navigation}>
173+
<button
174+
className={styles.navBtn}
175+
onClick={handlePreviousQuestion}
176+
disabled={currentIndex === 0}
177+
aria-label="Previous question"
178+
>
179+
← Previous
180+
</button>
181+
<button
182+
className={styles.navBtn}
183+
onClick={handleNextQuestion}
184+
aria-label={currentIndex === questions.length - 1 ? 'Finish quiz' : 'Next question'}
185+
>
186+
{currentIndex === questions.length - 1 ? 'Finish' : 'Next →'}
187+
</button>
188+
</div>
189+
)}
190+
</div>
191+
192+
{!isAnswered && (
193+
<div className={styles.hint}>
194+
Use arrow keys to navigate, Enter to select
195+
</div>
196+
)}
197+
</div>
198+
);
199+
}

0 commit comments

Comments
 (0)