Below are two component for app created in Next.js 14 and Shadcn UI.
Below are two component.
<code>"use client"
import React, { useEffect, useState, useRef, RefCallback } from 'react';
import { useParams } from 'next/navigation';
import { ChevronUp, ChevronDown, Menu } from 'lucide-react';
import Quiz from '@/components/quiz/Quiz';
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { useToast } from "@/components/ui/use-toast";
import { useUserStore } from '@/store/user-store';
import { Loader2 } from 'lucide-react';
import { fetchQuiz } from "@/lib/api";
interface Question {
questionId: string;
heading: string;
description: string;
options: Array<{
optionId: string;
description: string;
}>;
}
interface RightOption {
questionId: string;
rightOption: string[];
explanation: string;
}
const QuizAttempt: React.FC = () => {
const params = useParams();
const quizId = params.quizId as string;
const [questions, setQuestions] = useState<Question[]>([]);
const [selectedOptions, setSelectedOptions] = useState<Record<string, string[]>>({});
const [rightOptions, setRightOptions] = useState<RightOption[]>([]);
const [loading, setLoading] = useState(true);
const [submitted, setSubmitted] = useState(false);
const [buttonLoading, setButtonLoading] = useState(false);
const [timeRemaining, setTimeRemaining] = useState(20 * 60);
const questionRefs = useRef<Record<string, HTMLDivElement | null>>({});
const setQuestionRef: RefCallback<HTMLDivElement> = (element: HTMLDivElement | null) => {
if (element) {
questionRefs.current[element.id] = element;
}
};
const { user } = useUserStore();
const [quizSummary, setQuizSummary] = useState({ right: 0, wrong: 0, unattempted: 0 });
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showUpIcon, setShowUpIcon] = useState(false);
const [showDownIcon, setShowDownIcon] = useState(false);
const [showTrackingBar, setShowTrackingBar] = useState(false);
const { toast } = useToast();
useEffect(() => {
const fetchQuestions = async () => {
if (!quizId || !user) return;
try {
const data = await fetchQuiz(quizId);
setQuestions(data);
} catch (error) {
console.error("Error fetching questions:", error);
toast({
title: "Error",
description: "Failed to fetch quiz questions",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
fetchQuestions();
}, [quizId, user, toast]);
useEffect(() => {
const timer = setInterval(() => {
setTimeRemaining((prevTime) => {
if (prevTime <= 0) {
clearInterval(timer);
submitQuiz();
return 0;
}
return prevTime - 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const checkScroll = () => {
if (scrollContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
setShowUpIcon(scrollTop > 0);
setShowDownIcon(scrollTop + clientHeight < scrollHeight);
}
};
const container = scrollContainerRef.current;
if (container) {
container.addEventListener('scroll', checkScroll);
checkScroll();
return () => container.removeEventListener('scroll', checkScroll);
}
}, [questions]);
const handleOptionChange = (questionId: string, selectedOption: string[]) => {
console.log('Updating options for question:', questionId, 'New options:', selectedOption);
setSelectedOptions((prevSelectedOptions) => ({
...prevSelectedOptions,
[questionId]: selectedOption,
}));
};
useEffect(() => {
console.log('Updated Selected Options:', selectedOptions);
}, [selectedOptions]);
const submitQuiz = async () => {
if (!quizId || !user) return;
setButtonLoading(true);
const payload = Object.entries(selectedOptions).map(([questionId, optionIds]) => ({
questionId,
optionIds
}));
try {
const token = await user.getIdToken();
const response = await fetch(`/api/quiz/submit/${quizId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to submit quiz');
}
const data = await response.json();
setSubmitted(true);
setRightOptions(data.quizResponses);
// Calculate quiz summary
let right = 0;
let wrong = 0;
let unattempted = 0;
questions.forEach(question => {
const selectedOptionIds = selectedOptions[question.questionId] || [];
const correctOptionIds = data.quizResponses.find((q: RightOption) => q.questionId === question.questionId)?.rightOption || [];
if (selectedOptionIds.length === 0) {
unattempted++;
} else if (arraysEqual(selectedOptionIds.sort(), correctOptionIds.sort())) {
right++;
} else {
wrong++;
}
});
setQuizSummary({ right, wrong, unattempted });
} catch (error) {
console.error('Error:', error);
toast({
title: "Error",
description: "Failed to submit quiz",
variant: "destructive",
});
} finally {
setButtonLoading(false);
}
};
const arraysEqual = (a: string[], b: string[]) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
};
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
const scrollToQuestion = (questionId: string) => {
questionRefs.current[questionId]?.scrollIntoView({ behavior: 'smooth' });
setShowTrackingBar(false);
};
const scroll = (direction: 'up' | 'down') => {
const container = scrollContainerRef.current;
if (!container) return;
const scrollStep = direction === 'up' ? -1 : 1;
let start: number | undefined;
let previousTimestamp: number | undefined;
const step = (timestamp: number) => {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimestamp !== timestamp) {
const currentScrollTop = container.scrollTop;
container.scrollTop += scrollStep;
if (container.scrollTop === currentScrollTop) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = container;
setShowUpIcon(scrollTop > 0);
setShowDownIcon(scrollTop + clientHeight < scrollHeight);
}
if (elapsed < 2000) {
previousTimestamp = timestamp;
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
};
const toggleTrackingBar = () => {
setShowTrackingBar(!showTrackingBar);
};
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
return (
<div className="relative flex h-screen dark:bg-gray-900">
{/* Floating menu icon for mobile */}
<div className="fixed z-50 bottom-4 right-4 md:hidden">
<Button
onClick={toggleTrackingBar}
variant="outline"
size="icon"
>
<Menu className="h-4 w-4" />
</Button>
</div>
{/* Question tracking column with auto-scroll */}
<Card className={`fixed flex flex-col items-center w-64 h-2/3 left-6 top-1/4 transition-all duration-300 ease-in-out ${showTrackingBar ? 'translate-x-0' : '-translate-x-full'} md:translate-x-0 md:w-16`}>
{showUpIcon && (
<Button
variant="outline"
size="icon"
className="mb-2"
onMouseEnter={() => scroll('up')}
onMouseLeave={() => {}}
>
<ChevronUp className="h-4 w-4" />
</Button>
)}
<div
ref={scrollContainerRef}
className="flex-grow w-full p-2 overflow-y-auto scrollbar-hide"
>
{questions.map((question, index) => (
<Button
key={question.questionId}
variant={selectedOptions[question.questionId]?.length > 0 ? "secondary" : "outline"}
className="w-full mb-2"
onClick={() => scrollToQuestion(question.questionId)}
>
{index + 1}
</Button>
))}
</div>
{showDownIcon && (
<Button
variant="outline"
size="icon"
className="mt-2"
onMouseEnter={() => scroll('down')}
onMouseLeave={() => {}}
>
<ChevronDown className="h-4 w-4" />
</Button>
)}
</Card>
{/* Quiz content */}
<div className="flex-1 max-w-full p-2 md:p-4 md:ml-24 dark:text-white">
{questions.map((question) => {
return (
<div
key={question.questionId}
id={question.questionId}
className='mb-8 md:mb-12'
ref={setQuestionRef}
>
<Quiz
heading={question.heading}
description={question.description}
options={question.options}
questionId={question.questionId}
selectedOptions={selectedOptions[question.questionId] || []}
onOptionChange={handleOptionChange}
rightOptions={rightOptions}
submitted={submitted}
/>
</div>
);
})}
<div className='flex flex-col items-center justify-center pb-4'>
{submitted ? (
<Card className="p-4">
<h2 className="text-xl font-bold mb-2">Quiz Summary</h2>
<p>Correct: {quizSummary.right}</p>
<p>Incorrect: {quizSummary.wrong}</p>
<p>Unattempted: {quizSummary.unattempted}</p>
</Card>
) : (
<Button
onClick={submitQuiz}
disabled={buttonLoading}
>
{buttonLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Evaluating...
</>
) : (
'Submit Quiz'
)}
</Button>
)}
</div>
</div>
</div>
);
};
export default QuizAttempt;
</code>
<code>"use client"
import React, { useEffect, useState, useRef, RefCallback } from 'react';
import { useParams } from 'next/navigation';
import { ChevronUp, ChevronDown, Menu } from 'lucide-react';
import Quiz from '@/components/quiz/Quiz';
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { useToast } from "@/components/ui/use-toast";
import { useUserStore } from '@/store/user-store';
import { Loader2 } from 'lucide-react';
import { fetchQuiz } from "@/lib/api";
interface Question {
questionId: string;
heading: string;
description: string;
options: Array<{
optionId: string;
description: string;
}>;
}
interface RightOption {
questionId: string;
rightOption: string[];
explanation: string;
}
const QuizAttempt: React.FC = () => {
const params = useParams();
const quizId = params.quizId as string;
const [questions, setQuestions] = useState<Question[]>([]);
const [selectedOptions, setSelectedOptions] = useState<Record<string, string[]>>({});
const [rightOptions, setRightOptions] = useState<RightOption[]>([]);
const [loading, setLoading] = useState(true);
const [submitted, setSubmitted] = useState(false);
const [buttonLoading, setButtonLoading] = useState(false);
const [timeRemaining, setTimeRemaining] = useState(20 * 60);
const questionRefs = useRef<Record<string, HTMLDivElement | null>>({});
const setQuestionRef: RefCallback<HTMLDivElement> = (element: HTMLDivElement | null) => {
if (element) {
questionRefs.current[element.id] = element;
}
};
const { user } = useUserStore();
const [quizSummary, setQuizSummary] = useState({ right: 0, wrong: 0, unattempted: 0 });
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showUpIcon, setShowUpIcon] = useState(false);
const [showDownIcon, setShowDownIcon] = useState(false);
const [showTrackingBar, setShowTrackingBar] = useState(false);
const { toast } = useToast();
useEffect(() => {
const fetchQuestions = async () => {
if (!quizId || !user) return;
try {
const data = await fetchQuiz(quizId);
setQuestions(data);
} catch (error) {
console.error("Error fetching questions:", error);
toast({
title: "Error",
description: "Failed to fetch quiz questions",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
fetchQuestions();
}, [quizId, user, toast]);
useEffect(() => {
const timer = setInterval(() => {
setTimeRemaining((prevTime) => {
if (prevTime <= 0) {
clearInterval(timer);
submitQuiz();
return 0;
}
return prevTime - 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const checkScroll = () => {
if (scrollContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
setShowUpIcon(scrollTop > 0);
setShowDownIcon(scrollTop + clientHeight < scrollHeight);
}
};
const container = scrollContainerRef.current;
if (container) {
container.addEventListener('scroll', checkScroll);
checkScroll();
return () => container.removeEventListener('scroll', checkScroll);
}
}, [questions]);
const handleOptionChange = (questionId: string, selectedOption: string[]) => {
console.log('Updating options for question:', questionId, 'New options:', selectedOption);
setSelectedOptions((prevSelectedOptions) => ({
...prevSelectedOptions,
[questionId]: selectedOption,
}));
};
useEffect(() => {
console.log('Updated Selected Options:', selectedOptions);
}, [selectedOptions]);
const submitQuiz = async () => {
if (!quizId || !user) return;
setButtonLoading(true);
const payload = Object.entries(selectedOptions).map(([questionId, optionIds]) => ({
questionId,
optionIds
}));
try {
const token = await user.getIdToken();
const response = await fetch(`/api/quiz/submit/${quizId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to submit quiz');
}
const data = await response.json();
setSubmitted(true);
setRightOptions(data.quizResponses);
// Calculate quiz summary
let right = 0;
let wrong = 0;
let unattempted = 0;
questions.forEach(question => {
const selectedOptionIds = selectedOptions[question.questionId] || [];
const correctOptionIds = data.quizResponses.find((q: RightOption) => q.questionId === question.questionId)?.rightOption || [];
if (selectedOptionIds.length === 0) {
unattempted++;
} else if (arraysEqual(selectedOptionIds.sort(), correctOptionIds.sort())) {
right++;
} else {
wrong++;
}
});
setQuizSummary({ right, wrong, unattempted });
} catch (error) {
console.error('Error:', error);
toast({
title: "Error",
description: "Failed to submit quiz",
variant: "destructive",
});
} finally {
setButtonLoading(false);
}
};
const arraysEqual = (a: string[], b: string[]) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
};
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
const scrollToQuestion = (questionId: string) => {
questionRefs.current[questionId]?.scrollIntoView({ behavior: 'smooth' });
setShowTrackingBar(false);
};
const scroll = (direction: 'up' | 'down') => {
const container = scrollContainerRef.current;
if (!container) return;
const scrollStep = direction === 'up' ? -1 : 1;
let start: number | undefined;
let previousTimestamp: number | undefined;
const step = (timestamp: number) => {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimestamp !== timestamp) {
const currentScrollTop = container.scrollTop;
container.scrollTop += scrollStep;
if (container.scrollTop === currentScrollTop) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = container;
setShowUpIcon(scrollTop > 0);
setShowDownIcon(scrollTop + clientHeight < scrollHeight);
}
if (elapsed < 2000) {
previousTimestamp = timestamp;
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
};
const toggleTrackingBar = () => {
setShowTrackingBar(!showTrackingBar);
};
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
return (
<div className="relative flex h-screen dark:bg-gray-900">
{/* Floating menu icon for mobile */}
<div className="fixed z-50 bottom-4 right-4 md:hidden">
<Button
onClick={toggleTrackingBar}
variant="outline"
size="icon"
>
<Menu className="h-4 w-4" />
</Button>
</div>
{/* Question tracking column with auto-scroll */}
<Card className={`fixed flex flex-col items-center w-64 h-2/3 left-6 top-1/4 transition-all duration-300 ease-in-out ${showTrackingBar ? 'translate-x-0' : '-translate-x-full'} md:translate-x-0 md:w-16`}>
{showUpIcon && (
<Button
variant="outline"
size="icon"
className="mb-2"
onMouseEnter={() => scroll('up')}
onMouseLeave={() => {}}
>
<ChevronUp className="h-4 w-4" />
</Button>
)}
<div
ref={scrollContainerRef}
className="flex-grow w-full p-2 overflow-y-auto scrollbar-hide"
>
{questions.map((question, index) => (
<Button
key={question.questionId}
variant={selectedOptions[question.questionId]?.length > 0 ? "secondary" : "outline"}
className="w-full mb-2"
onClick={() => scrollToQuestion(question.questionId)}
>
{index + 1}
</Button>
))}
</div>
{showDownIcon && (
<Button
variant="outline"
size="icon"
className="mt-2"
onMouseEnter={() => scroll('down')}
onMouseLeave={() => {}}
>
<ChevronDown className="h-4 w-4" />
</Button>
)}
</Card>
{/* Quiz content */}
<div className="flex-1 max-w-full p-2 md:p-4 md:ml-24 dark:text-white">
{questions.map((question) => {
return (
<div
key={question.questionId}
id={question.questionId}
className='mb-8 md:mb-12'
ref={setQuestionRef}
>
<Quiz
heading={question.heading}
description={question.description}
options={question.options}
questionId={question.questionId}
selectedOptions={selectedOptions[question.questionId] || []}
onOptionChange={handleOptionChange}
rightOptions={rightOptions}
submitted={submitted}
/>
</div>
);
})}
<div className='flex flex-col items-center justify-center pb-4'>
{submitted ? (
<Card className="p-4">
<h2 className="text-xl font-bold mb-2">Quiz Summary</h2>
<p>Correct: {quizSummary.right}</p>
<p>Incorrect: {quizSummary.wrong}</p>
<p>Unattempted: {quizSummary.unattempted}</p>
</Card>
) : (
<Button
onClick={submitQuiz}
disabled={buttonLoading}
>
{buttonLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Evaluating...
</>
) : (
'Submit Quiz'
)}
</Button>
)}
</div>
</div>
</div>
);
};
export default QuizAttempt;
</code>
"use client"
import React, { useEffect, useState, useRef, RefCallback } from 'react';
import { useParams } from 'next/navigation';
import { ChevronUp, ChevronDown, Menu } from 'lucide-react';
import Quiz from '@/components/quiz/Quiz';
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { useToast } from "@/components/ui/use-toast";
import { useUserStore } from '@/store/user-store';
import { Loader2 } from 'lucide-react';
import { fetchQuiz } from "@/lib/api";
interface Question {
questionId: string;
heading: string;
description: string;
options: Array<{
optionId: string;
description: string;
}>;
}
interface RightOption {
questionId: string;
rightOption: string[];
explanation: string;
}
const QuizAttempt: React.FC = () => {
const params = useParams();
const quizId = params.quizId as string;
const [questions, setQuestions] = useState<Question[]>([]);
const [selectedOptions, setSelectedOptions] = useState<Record<string, string[]>>({});
const [rightOptions, setRightOptions] = useState<RightOption[]>([]);
const [loading, setLoading] = useState(true);
const [submitted, setSubmitted] = useState(false);
const [buttonLoading, setButtonLoading] = useState(false);
const [timeRemaining, setTimeRemaining] = useState(20 * 60);
const questionRefs = useRef<Record<string, HTMLDivElement | null>>({});
const setQuestionRef: RefCallback<HTMLDivElement> = (element: HTMLDivElement | null) => {
if (element) {
questionRefs.current[element.id] = element;
}
};
const { user } = useUserStore();
const [quizSummary, setQuizSummary] = useState({ right: 0, wrong: 0, unattempted: 0 });
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showUpIcon, setShowUpIcon] = useState(false);
const [showDownIcon, setShowDownIcon] = useState(false);
const [showTrackingBar, setShowTrackingBar] = useState(false);
const { toast } = useToast();
useEffect(() => {
const fetchQuestions = async () => {
if (!quizId || !user) return;
try {
const data = await fetchQuiz(quizId);
setQuestions(data);
} catch (error) {
console.error("Error fetching questions:", error);
toast({
title: "Error",
description: "Failed to fetch quiz questions",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
fetchQuestions();
}, [quizId, user, toast]);
useEffect(() => {
const timer = setInterval(() => {
setTimeRemaining((prevTime) => {
if (prevTime <= 0) {
clearInterval(timer);
submitQuiz();
return 0;
}
return prevTime - 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const checkScroll = () => {
if (scrollContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
setShowUpIcon(scrollTop > 0);
setShowDownIcon(scrollTop + clientHeight < scrollHeight);
}
};
const container = scrollContainerRef.current;
if (container) {
container.addEventListener('scroll', checkScroll);
checkScroll();
return () => container.removeEventListener('scroll', checkScroll);
}
}, [questions]);
const handleOptionChange = (questionId: string, selectedOption: string[]) => {
console.log('Updating options for question:', questionId, 'New options:', selectedOption);
setSelectedOptions((prevSelectedOptions) => ({
...prevSelectedOptions,
[questionId]: selectedOption,
}));
};
useEffect(() => {
console.log('Updated Selected Options:', selectedOptions);
}, [selectedOptions]);
const submitQuiz = async () => {
if (!quizId || !user) return;
setButtonLoading(true);
const payload = Object.entries(selectedOptions).map(([questionId, optionIds]) => ({
questionId,
optionIds
}));
try {
const token = await user.getIdToken();
const response = await fetch(`/api/quiz/submit/${quizId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to submit quiz');
}
const data = await response.json();
setSubmitted(true);
setRightOptions(data.quizResponses);
// Calculate quiz summary
let right = 0;
let wrong = 0;
let unattempted = 0;
questions.forEach(question => {
const selectedOptionIds = selectedOptions[question.questionId] || [];
const correctOptionIds = data.quizResponses.find((q: RightOption) => q.questionId === question.questionId)?.rightOption || [];
if (selectedOptionIds.length === 0) {
unattempted++;
} else if (arraysEqual(selectedOptionIds.sort(), correctOptionIds.sort())) {
right++;
} else {
wrong++;
}
});
setQuizSummary({ right, wrong, unattempted });
} catch (error) {
console.error('Error:', error);
toast({
title: "Error",
description: "Failed to submit quiz",
variant: "destructive",
});
} finally {
setButtonLoading(false);
}
};
const arraysEqual = (a: string[], b: string[]) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
};
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
const scrollToQuestion = (questionId: string) => {
questionRefs.current[questionId]?.scrollIntoView({ behavior: 'smooth' });
setShowTrackingBar(false);
};
const scroll = (direction: 'up' | 'down') => {
const container = scrollContainerRef.current;
if (!container) return;
const scrollStep = direction === 'up' ? -1 : 1;
let start: number | undefined;
let previousTimestamp: number | undefined;
const step = (timestamp: number) => {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimestamp !== timestamp) {
const currentScrollTop = container.scrollTop;
container.scrollTop += scrollStep;
if (container.scrollTop === currentScrollTop) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = container;
setShowUpIcon(scrollTop > 0);
setShowDownIcon(scrollTop + clientHeight < scrollHeight);
}
if (elapsed < 2000) {
previousTimestamp = timestamp;
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
};
const toggleTrackingBar = () => {
setShowTrackingBar(!showTrackingBar);
};
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
return (
<div className="relative flex h-screen dark:bg-gray-900">
{/* Floating menu icon for mobile */}
<div className="fixed z-50 bottom-4 right-4 md:hidden">
<Button
onClick={toggleTrackingBar}
variant="outline"
size="icon"
>
<Menu className="h-4 w-4" />
</Button>
</div>
{/* Question tracking column with auto-scroll */}
<Card className={`fixed flex flex-col items-center w-64 h-2/3 left-6 top-1/4 transition-all duration-300 ease-in-out ${showTrackingBar ? 'translate-x-0' : '-translate-x-full'} md:translate-x-0 md:w-16`}>
{showUpIcon && (
<Button
variant="outline"
size="icon"
className="mb-2"
onMouseEnter={() => scroll('up')}
onMouseLeave={() => {}}
>
<ChevronUp className="h-4 w-4" />
</Button>
)}
<div
ref={scrollContainerRef}
className="flex-grow w-full p-2 overflow-y-auto scrollbar-hide"
>
{questions.map((question, index) => (
<Button
key={question.questionId}
variant={selectedOptions[question.questionId]?.length > 0 ? "secondary" : "outline"}
className="w-full mb-2"
onClick={() => scrollToQuestion(question.questionId)}
>
{index + 1}
</Button>
))}
</div>
{showDownIcon && (
<Button
variant="outline"
size="icon"
className="mt-2"
onMouseEnter={() => scroll('down')}
onMouseLeave={() => {}}
>
<ChevronDown className="h-4 w-4" />
</Button>
)}
</Card>
{/* Quiz content */}
<div className="flex-1 max-w-full p-2 md:p-4 md:ml-24 dark:text-white">
{questions.map((question) => {
return (
<div
key={question.questionId}
id={question.questionId}
className='mb-8 md:mb-12'
ref={setQuestionRef}
>
<Quiz
heading={question.heading}
description={question.description}
options={question.options}
questionId={question.questionId}
selectedOptions={selectedOptions[question.questionId] || []}
onOptionChange={handleOptionChange}
rightOptions={rightOptions}
submitted={submitted}
/>
</div>
);
})}
<div className='flex flex-col items-center justify-center pb-4'>
{submitted ? (
<Card className="p-4">
<h2 className="text-xl font-bold mb-2">Quiz Summary</h2>
<p>Correct: {quizSummary.right}</p>
<p>Incorrect: {quizSummary.wrong}</p>
<p>Unattempted: {quizSummary.unattempted}</p>
</Card>
) : (
<Button
onClick={submitQuiz}
disabled={buttonLoading}
>
{buttonLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Evaluating...
</>
) : (
'Submit Quiz'
)}
</Button>
)}
</div>
</div>
</div>
);
};
export default QuizAttempt;
Below i have added Quiz component.
<code>import React, { useCallback } from "react";
import { motion } from "framer-motion";
import { Check, X, Circle, CircleCheck } from 'lucide-react';
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import MarkdownRenderer from "@/components/markdown/MarkdownRenderer";
interface Option {
optionId: string;
description: string;
}
interface QuizProps {
heading: string;
description: string;
options: Option[];
questionId: string;
selectedOptions: string[];
onOptionChange: (questionId: string, selectedOption: string[]) => void;
rightOptions: Array<{
questionId: string;
rightOption: string[];
explanation: string;
}> | null;
submitted: boolean;
}
const Quiz: React.FC<QuizProps> = ({
heading,
description,
options,
questionId,
selectedOptions,
onOptionChange,
rightOptions,
submitted
}) => {
const filteredQuestion = rightOptions?.find(item => item.questionId === questionId);
const handleOptionChange = useCallback((optionId: string) => {
console.log("Changing option for question:", questionId, "Option:", optionId);
const newSelected = selectedOptions.includes(optionId)
? selectedOptions.filter(id => id !== optionId)
: [...selectedOptions, optionId];
console.log("New selected options:", newSelected);
onOptionChange(questionId, newSelected);
}, [questionId, selectedOptions, onOptionChange]);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="overflow-hidden">
<CardContent className="p-6">
<h2 className="text-xl font-bold mb-4">{heading}</h2>
<div className="mb-6">
<MarkdownRenderer>{description}</MarkdownRenderer>
</div>
<div className="space-y-2">
{options.map((option) => (
<Selection
key={`${questionId}-${option.optionId}`}
optionId={option.optionId}
description={option.description}
selected={selectedOptions}
rightOptions={filteredQuestion?.rightOption || []}
submitted={submitted}
onChange={() => handleOptionChange(option.optionId)}
/>
))}
</div>
{submitted && filteredQuestion && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{ duration: 0.5 }}
className="mt-6 p-4 rounded-md bg-secondary"
>
<p className="font-semibold">Explanation:</p>
<p>{filteredQuestion.explanation}</p>
</motion.div>
)}
</CardContent>
</Card>
</motion.div>
);
};
interface SelectionProps {
optionId: string;
description: string;
selected: string[];
rightOptions: string[];
submitted: boolean;
onChange: () => void;
}
const Selection: React.FC<SelectionProps> = ({
optionId,
description,
selected,
rightOptions,
submitted,
onChange
}) => {
const isSelected = selected.includes(optionId);
const isRightOption = rightOptions.includes(optionId);
const isAttempted = selected.length > 0;
const getOptionColor = () => {
if (isSelected && isRightOption && submitted) return 'border-green-600 dark:border-green-400 bg-green-50 dark:bg-green-900';
if (isSelected && !isRightOption && submitted) return 'border-red-600 dark:border-red-400 bg-red-50 dark:bg-red-900';
if (isSelected) return 'border-primary bg-primary/10';
if (!isSelected && isRightOption && submitted && !isAttempted) return 'border-blue-600 dark:border-blue-400 bg-blue-50 dark:bg-blue-900';
if (!isSelected && isRightOption && submitted) return 'border-green-600 dark:border-green-400 bg-green-50 dark:bg-green-900';
return 'border-input';
};
const getIcon = () => {
if (isSelected && isRightOption && submitted) return <Check className="text-green-600 dark:text-green-400" />;
if (isSelected && !isRightOption && submitted) return <X className="text-red-600 dark:text-red-400" />;
if (!isSelected && isRightOption && submitted) return <Check className="text-green-600 dark:text-green-400" />;
if (isSelected && !submitted) return <CircleCheck className="text-primary" />;
return <Circle className="text-muted-foreground" />;
};
return (
<div className={cn(
"flex items-center space-x-2 rounded-md border p-4",
getOptionColor(),
submitted ? "cursor-not-allowed" : "cursor-pointer"
)}>
<Checkbox
id={optionId}
checked={isSelected}
onCheckedChange={onChange}
disabled={submitted}
className="sr-only"
/>
<Label
htmlFor={optionId}
className="flex flex-1 items-center space-x-3 text-sm font-normal"
>
<span className="flex-shrink-0">{getIcon()}</span>
<span>{description}</span>
</Label>
</div>
);
};
export default Quiz;
</code>
<code>import React, { useCallback } from "react";
import { motion } from "framer-motion";
import { Check, X, Circle, CircleCheck } from 'lucide-react';
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import MarkdownRenderer from "@/components/markdown/MarkdownRenderer";
interface Option {
optionId: string;
description: string;
}
interface QuizProps {
heading: string;
description: string;
options: Option[];
questionId: string;
selectedOptions: string[];
onOptionChange: (questionId: string, selectedOption: string[]) => void;
rightOptions: Array<{
questionId: string;
rightOption: string[];
explanation: string;
}> | null;
submitted: boolean;
}
const Quiz: React.FC<QuizProps> = ({
heading,
description,
options,
questionId,
selectedOptions,
onOptionChange,
rightOptions,
submitted
}) => {
const filteredQuestion = rightOptions?.find(item => item.questionId === questionId);
const handleOptionChange = useCallback((optionId: string) => {
console.log("Changing option for question:", questionId, "Option:", optionId);
const newSelected = selectedOptions.includes(optionId)
? selectedOptions.filter(id => id !== optionId)
: [...selectedOptions, optionId];
console.log("New selected options:", newSelected);
onOptionChange(questionId, newSelected);
}, [questionId, selectedOptions, onOptionChange]);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="overflow-hidden">
<CardContent className="p-6">
<h2 className="text-xl font-bold mb-4">{heading}</h2>
<div className="mb-6">
<MarkdownRenderer>{description}</MarkdownRenderer>
</div>
<div className="space-y-2">
{options.map((option) => (
<Selection
key={`${questionId}-${option.optionId}`}
optionId={option.optionId}
description={option.description}
selected={selectedOptions}
rightOptions={filteredQuestion?.rightOption || []}
submitted={submitted}
onChange={() => handleOptionChange(option.optionId)}
/>
))}
</div>
{submitted && filteredQuestion && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{ duration: 0.5 }}
className="mt-6 p-4 rounded-md bg-secondary"
>
<p className="font-semibold">Explanation:</p>
<p>{filteredQuestion.explanation}</p>
</motion.div>
)}
</CardContent>
</Card>
</motion.div>
);
};
interface SelectionProps {
optionId: string;
description: string;
selected: string[];
rightOptions: string[];
submitted: boolean;
onChange: () => void;
}
const Selection: React.FC<SelectionProps> = ({
optionId,
description,
selected,
rightOptions,
submitted,
onChange
}) => {
const isSelected = selected.includes(optionId);
const isRightOption = rightOptions.includes(optionId);
const isAttempted = selected.length > 0;
const getOptionColor = () => {
if (isSelected && isRightOption && submitted) return 'border-green-600 dark:border-green-400 bg-green-50 dark:bg-green-900';
if (isSelected && !isRightOption && submitted) return 'border-red-600 dark:border-red-400 bg-red-50 dark:bg-red-900';
if (isSelected) return 'border-primary bg-primary/10';
if (!isSelected && isRightOption && submitted && !isAttempted) return 'border-blue-600 dark:border-blue-400 bg-blue-50 dark:bg-blue-900';
if (!isSelected && isRightOption && submitted) return 'border-green-600 dark:border-green-400 bg-green-50 dark:bg-green-900';
return 'border-input';
};
const getIcon = () => {
if (isSelected && isRightOption && submitted) return <Check className="text-green-600 dark:text-green-400" />;
if (isSelected && !isRightOption && submitted) return <X className="text-red-600 dark:text-red-400" />;
if (!isSelected && isRightOption && submitted) return <Check className="text-green-600 dark:text-green-400" />;
if (isSelected && !submitted) return <CircleCheck className="text-primary" />;
return <Circle className="text-muted-foreground" />;
};
return (
<div className={cn(
"flex items-center space-x-2 rounded-md border p-4",
getOptionColor(),
submitted ? "cursor-not-allowed" : "cursor-pointer"
)}>
<Checkbox
id={optionId}
checked={isSelected}
onCheckedChange={onChange}
disabled={submitted}
className="sr-only"
/>
<Label
htmlFor={optionId}
className="flex flex-1 items-center space-x-3 text-sm font-normal"
>
<span className="flex-shrink-0">{getIcon()}</span>
<span>{description}</span>
</Label>
</div>
);
};
export default Quiz;
</code>
import React, { useCallback } from "react";
import { motion } from "framer-motion";
import { Check, X, Circle, CircleCheck } from 'lucide-react';
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import MarkdownRenderer from "@/components/markdown/MarkdownRenderer";
interface Option {
optionId: string;
description: string;
}
interface QuizProps {
heading: string;
description: string;
options: Option[];
questionId: string;
selectedOptions: string[];
onOptionChange: (questionId: string, selectedOption: string[]) => void;
rightOptions: Array<{
questionId: string;
rightOption: string[];
explanation: string;
}> | null;
submitted: boolean;
}
const Quiz: React.FC<QuizProps> = ({
heading,
description,
options,
questionId,
selectedOptions,
onOptionChange,
rightOptions,
submitted
}) => {
const filteredQuestion = rightOptions?.find(item => item.questionId === questionId);
const handleOptionChange = useCallback((optionId: string) => {
console.log("Changing option for question:", questionId, "Option:", optionId);
const newSelected = selectedOptions.includes(optionId)
? selectedOptions.filter(id => id !== optionId)
: [...selectedOptions, optionId];
console.log("New selected options:", newSelected);
onOptionChange(questionId, newSelected);
}, [questionId, selectedOptions, onOptionChange]);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="overflow-hidden">
<CardContent className="p-6">
<h2 className="text-xl font-bold mb-4">{heading}</h2>
<div className="mb-6">
<MarkdownRenderer>{description}</MarkdownRenderer>
</div>
<div className="space-y-2">
{options.map((option) => (
<Selection
key={`${questionId}-${option.optionId}`}
optionId={option.optionId}
description={option.description}
selected={selectedOptions}
rightOptions={filteredQuestion?.rightOption || []}
submitted={submitted}
onChange={() => handleOptionChange(option.optionId)}
/>
))}
</div>
{submitted && filteredQuestion && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{ duration: 0.5 }}
className="mt-6 p-4 rounded-md bg-secondary"
>
<p className="font-semibold">Explanation:</p>
<p>{filteredQuestion.explanation}</p>
</motion.div>
)}
</CardContent>
</Card>
</motion.div>
);
};
interface SelectionProps {
optionId: string;
description: string;
selected: string[];
rightOptions: string[];
submitted: boolean;
onChange: () => void;
}
const Selection: React.FC<SelectionProps> = ({
optionId,
description,
selected,
rightOptions,
submitted,
onChange
}) => {
const isSelected = selected.includes(optionId);
const isRightOption = rightOptions.includes(optionId);
const isAttempted = selected.length > 0;
const getOptionColor = () => {
if (isSelected && isRightOption && submitted) return 'border-green-600 dark:border-green-400 bg-green-50 dark:bg-green-900';
if (isSelected && !isRightOption && submitted) return 'border-red-600 dark:border-red-400 bg-red-50 dark:bg-red-900';
if (isSelected) return 'border-primary bg-primary/10';
if (!isSelected && isRightOption && submitted && !isAttempted) return 'border-blue-600 dark:border-blue-400 bg-blue-50 dark:bg-blue-900';
if (!isSelected && isRightOption && submitted) return 'border-green-600 dark:border-green-400 bg-green-50 dark:bg-green-900';
return 'border-input';
};
const getIcon = () => {
if (isSelected && isRightOption && submitted) return <Check className="text-green-600 dark:text-green-400" />;
if (isSelected && !isRightOption && submitted) return <X className="text-red-600 dark:text-red-400" />;
if (!isSelected && isRightOption && submitted) return <Check className="text-green-600 dark:text-green-400" />;
if (isSelected && !submitted) return <CircleCheck className="text-primary" />;
return <Circle className="text-muted-foreground" />;
};
return (
<div className={cn(
"flex items-center space-x-2 rounded-md border p-4",
getOptionColor(),
submitted ? "cursor-not-allowed" : "cursor-pointer"
)}>
<Checkbox
id={optionId}
checked={isSelected}
onCheckedChange={onChange}
disabled={submitted}
className="sr-only"
/>
<Label
htmlFor={optionId}
className="flex flex-1 items-center space-x-3 text-sm font-normal"
>
<span className="flex-shrink-0">{getIcon()}</span>
<span>{description}</span>
</Label>
</div>
);
};
export default Quiz;
Issue is when multiple Quiz component is rendered when i change option of 2,3… component it always change value of 1st component.
<code> const handleOptionChange = useCallback((optionId: string) => {
console.log("Changing option for question:", questionId, "Option:", optionId);
const newSelected = selectedOptions.includes(optionId)
? selectedOptions.filter(id => id !== optionId)
: [...selectedOptions, optionId];
console.log("New selected options:", newSelected);
onOptionChange(questionId, newSelected);
}, [questionId, selectedOptions, onOptionChange]);
</code>
<code> const handleOptionChange = useCallback((optionId: string) => {
console.log("Changing option for question:", questionId, "Option:", optionId);
const newSelected = selectedOptions.includes(optionId)
? selectedOptions.filter(id => id !== optionId)
: [...selectedOptions, optionId];
console.log("New selected options:", newSelected);
onOptionChange(questionId, newSelected);
}, [questionId, selectedOptions, onOptionChange]);
</code>
const handleOptionChange = useCallback((optionId: string) => {
console.log("Changing option for question:", questionId, "Option:", optionId);
const newSelected = selectedOptions.includes(optionId)
? selectedOptions.filter(id => id !== optionId)
: [...selectedOptions, optionId];
console.log("New selected options:", newSelected);
onOptionChange(questionId, newSelected);
}, [questionId, selectedOptions, onOptionChange]);
Above method is always picking first component questionId.
1