Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions docs/examples/QuizExample.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
id: quiz-example
title: Quiz Component
description: Interactive quiz component for assessments
---

import Quiz from '@site/src/components/Quiz';

# Quiz Component Example

The Quiz component provides an interactive assessment experience with multiple-choice questions, immediate feedback, and keyboard accessibility.

## Features

- Multiple-choice questions with answer reveal
- Explanations after each answer
- Score summary at completion
- Keyboard navigation (arrow keys, Enter)
- Progress tracking
- Responsive design
- Accessibility features (ARIA labels, screen reader support)

## Usage

```jsx
import Quiz from '@site/src/components/Quiz';

const questions = [
{
question: "What is the output of 2 + 2 * 3?",
options: ["8", "12", "10", "7"],
correct: 0,
explanation: "Following order of operations, multiplication is performed before addition: 2 + (2 * 3) = 2 + 6 = 8"
},
{
question: "Which of these is not a primitive type in JavaScript?",
options: ["string", "number", "array", "boolean"],
correct: 2,
explanation: "Arrays are objects in JavaScript, not primitive types. Primitive types include string, number, boolean, undefined, null, and symbol."
}
];

export default function Page() {
return <Quiz questions={questions} />;
}
```

## Question Structure

Each question object must have:

- `question` (string): The question text
- `options` (array of strings): The available answer choices
- `correct` (number): Index of the correct answer (0-based)
- `explanation` (string): Explanation shown after answering

## Keyboard Shortcuts

When a question hasn't been answered:
- **↑ / ↓ Arrow Keys**: Navigate between options
- **Enter**: Select focused option

When a question is answered:
- **← / → Arrow Keys**: Navigate to previous/next question
- **Home**: Jump to first question
- **End**: Jump to last question

## Example Quiz

<Quiz questions={[
{
question: "What does HTML stand for?",
options: [
"Hyper Text Markup Language",
"High Tech Modern Language",
"Home Tool Markup Language",
"Hyperlinks and Text Markup Language"
],
correct: 0,
explanation: "HTML (HyperText Markup Language) is the standard markup language used to create web pages."
},
{
question: "Which CSS property is used to control text spacing?",
options: [
"text-padding",
"letter-spacing",
"word-distance",
"character-gap"
],
correct: 1,
explanation: "The letter-spacing property controls the space between characters in text. For space between words, use word-spacing."
},
{
question: "What is the primary purpose of JavaScript in web development?",
options: [
"Styling web pages",
"Adding interactivity and dynamic behavior",
"Managing server databases",
"Creating HTML structure"
],
correct: 1,
explanation: "JavaScript is used to add interactivity, handle user events, manipulate the DOM, and create dynamic behavior in web applications."
}
]} />

## Customization

The component uses CSS custom properties for theming. You can override these in your custom CSS:

- `--ifm-color-primary`: Primary button and accent color
- `--ifm-color-success`: Correct answer highlight
- `--ifm-color-danger`: Incorrect answer highlight
- `--ifm-color-info`: Selected option highlight
199 changes: 199 additions & 0 deletions src/components/Quiz.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { useState, useCallback, useEffect } from 'react';
import styles from './Quiz.module.css';

export default function Quiz({ questions = [] }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [selectedAnswers, setSelectedAnswers] = useState({});
const [showExplanations, setShowExplanations] = useState({});
const [isComplete, setIsComplete] = useState(false);
const [focusedOption, setFocusedOption] = useState(null);

if (!questions || questions.length === 0) {
return <div className={styles.empty}>No questions available</div>;
}

const currentQuestion = questions[currentIndex];
const currentAnswer = selectedAnswers[currentIndex];
const isAnswered = currentIndex in selectedAnswers;
const correctAnswer = currentQuestion.correct;
const isCorrect = currentAnswer === correctAnswer;

const score = Object.entries(selectedAnswers).filter(([idx, answer]) => {
return questions[parseInt(idx)].correct === answer;
}).length;

const handleSelectAnswer = useCallback((optionIndex) => {
if (!isAnswered) {
setSelectedAnswers(prev => ({
...prev,
[currentIndex]: optionIndex
}));
setShowExplanations(prev => ({
...prev,
[currentIndex]: true
}));
setFocusedOption(null);
}
}, [currentIndex, isAnswered]);

const handleKeyDown = useCallback((e) => {
if (isAnswered) {
if (e.key === 'ArrowRight' && currentIndex < questions.length - 1) {
setCurrentIndex(currentIndex + 1);
setFocusedOption(null);
} else if (e.key === 'ArrowLeft' && currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
setFocusedOption(null);
} else if (e.key === 'End') {
setCurrentIndex(questions.length - 1);
setFocusedOption(null);
} else if (e.key === 'Home') {
setCurrentIndex(0);
setFocusedOption(null);
}
return;
}

if (e.key === 'ArrowDown') {
e.preventDefault();
setFocusedOption(prev => {
const next = (prev === null ? 0 : Math.min(prev + 1, currentQuestion.options.length - 1));
return next;
});
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setFocusedOption(prev => {
const next = (prev === null ? 0 : Math.max(prev - 1, 0));
return prev;
});
} else if (e.key === 'Enter' && focusedOption !== null) {
e.preventDefault();
handleSelectAnswer(focusedOption);
}
}, [currentIndex, isAnswered, focusedOption, questions.length, currentQuestion.options.length, handleSelectAnswer]);

useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);

const handleNextQuestion = () => {
if (currentIndex < questions.length - 1) {
setCurrentIndex(currentIndex + 1);
setFocusedOption(null);
} else {
setIsComplete(true);
}
};

const handlePreviousQuestion = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
setFocusedOption(null);
}
};

const handleRestart = () => {
setCurrentIndex(0);
setSelectedAnswers({});
setShowExplanations({});
setIsComplete(false);
setFocusedOption(null);
};

if (isComplete) {
const percentage = Math.round((score / questions.length) * 100);
return (
<div className={styles.container}>
<div className={styles.summary}>
<h2>Quiz Complete!</h2>
<div className={styles.scoreCard}>
<div className={styles.scoreCircle}>{percentage}%</div>
<p className={styles.scoreText}>You got {score} out of {questions.length} correct</p>
</div>
<button className={styles.restartBtn} onClick={handleRestart}>
Restart Quiz
</button>
</div>
</div>
);
}

return (
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.progress}>
Question {currentIndex + 1} of {questions.length}
</div>
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ width: `${((currentIndex + 1) / questions.length) * 100}%` }}
/>
</div>
</div>

<div className={styles.questionCard}>
<h3 className={styles.question}>{currentQuestion.question}</h3>

<div className={styles.optionsContainer}>
{currentQuestion.options.map((option, idx) => {
const isSelected = currentAnswer === idx;
const isOptCorrect = correctAnswer === idx;
const showAsCorrect = isAnswered && isOptCorrect;
const showAsIncorrect = isAnswered && isSelected && !isOptCorrect;
const isFocused = focusedOption === idx && !isAnswered;

return (
<button
key={idx}
className={`${styles.option} ${isSelected ? styles.selected : ''} ${showAsCorrect ? styles.correct : ''} ${showAsIncorrect ? styles.incorrect : ''} ${isFocused ? styles.focused : ''}`}
onClick={() => handleSelectAnswer(idx)}
disabled={isAnswered}
aria-pressed={isSelected}
aria-label={`Option ${String.fromCharCode(65 + idx)}: ${option}`}
>
<span className={styles.optionLabel}>{String.fromCharCode(65 + idx)}</span>
<span className={styles.optionText}>{option}</span>
{isAnswered && isOptCorrect && <span className={styles.checkmark}>✓</span>}
{isAnswered && showAsIncorrect && <span className={styles.xmark}>✗</span>}
</button>
);
})}
</div>

{showExplanations[currentIndex] && (
<div className={`${styles.explanation} ${isCorrect ? styles.successExplanation : styles.errorExplanation}`}>
<strong>{isCorrect ? 'Correct!' : 'Incorrect.'}</strong> {currentQuestion.explanation}
</div>
)}

{isAnswered && (
<div className={styles.navigation}>
<button
className={styles.navBtn}
onClick={handlePreviousQuestion}
disabled={currentIndex === 0}
aria-label="Previous question"
>
← Previous
</button>
<button
className={styles.navBtn}
onClick={handleNextQuestion}
aria-label={currentIndex === questions.length - 1 ? 'Finish quiz' : 'Next question'}
>
{currentIndex === questions.length - 1 ? 'Finish' : 'Next →'}
</button>
</div>
)}
</div>

{!isAnswered && (
<div className={styles.hint}>
Use arrow keys to navigate, Enter to select
</div>
)}
</div>
);
}
Loading
Loading