pvyas96's picture
Upload 19 files
89f2e0a verified
import React, { useState, useEffect, useCallback } from 'react';
import { Quiz, QuizConfig, UserAnswer, QuizQuestion as QuizQuestionType } from '../types.ts';
import Calculator from './Calculator.tsx';
interface QuestionPaletteProps {
count: number;
currentIndex: number;
answers: UserAnswer[];
onQuestionSelect: (index: number) => void;
}
const QuestionPalette: React.FC<QuestionPaletteProps> = ({ count, currentIndex, answers, onQuestionSelect }) => {
return (
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-700 dark:text-gray-300 mb-3 text-center">Question Navigation</h4>
<div className="grid grid-cols-5 gap-2">
{Array.from({ length: count }, (_, i) => {
const isAnswered = answers[i]?.selectedOption !== null;
const isCurrent = i === currentIndex;
let buttonClass = 'bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200';
if (isAnswered) {
buttonClass = 'bg-green-100 dark:bg-green-900/50 hover:bg-green-200 dark:hover:bg-green-800/50 text-green-800 dark:text-green-300';
}
if (isCurrent) {
buttonClass = 'bg-blue-500 hover:bg-blue-600 text-white ring-2 ring-offset-2 ring-offset-white dark:ring-offset-gray-800 ring-blue-500';
}
return (
<button
key={i}
onClick={() => onQuestionSelect(i)}
className={`w-10 h-10 rounded-lg flex items-center justify-center font-bold text-sm transition-all duration-200 ${buttonClass}`}
aria-label={`Go to question ${i + 1}`}
>
{i + 1}
</button>
);
})}
</div>
</div>
);
};
const Question: React.FC<{ question: QuizQuestionType; questionNumber: number; totalQuestions: number; }> = ({ question, questionNumber, totalQuestions }) => {
return (
<div>
<div className="text-gray-800 dark:text-gray-100 text-xl whitespace-pre-wrap bg-gray-50 dark:bg-gray-700/50 p-6 rounded-lg border border-gray-200 dark:border-gray-700">{question.questionText}</div>
{question.questionType === 'MSQ' && (
<p className="text-base text-blue-600 dark:text-blue-400 mt-2 font-semibold">
(Select all that apply)
</p>
)}
</div>
);
};
interface QuizScreenProps {
quiz: Quiz;
config: QuizConfig;
onSubmit: (answers: UserAnswer[]) => void;
onQuit: () => void;
}
const QuizScreen: React.FC<QuizScreenProps> = ({ quiz, config, onSubmit, onQuit }) => {
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [answers, setAnswers] = useState<UserAnswer[]>(() => Array(quiz.questions.length).fill({ selectedOption: null }));
const [timeLeft, setTimeLeft] = useState(config.timeLimit * 60);
const handleSubmit = useCallback(() => {
onSubmit(answers);
}, [answers, onSubmit]);
const handleQuit = () => {
if (window.confirm("Are you sure you want to quit the quiz? All progress will be lost.")) {
onQuit();
}
};
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(prevTime => {
if (prevTime <= 1) {
clearInterval(timer);
handleSubmit();
return 0;
}
return prevTime - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [handleSubmit]);
const handleAnswerSelect = (optionIndex: number) => {
const newAnswers = [...answers];
const currentQuestion = quiz.questions[currentQuestionIndex];
if (currentQuestion.questionType === 'MSQ') {
const currentSelection = answers[currentQuestionIndex].selectedOption || [];
const newSelection: number[] = [...currentSelection];
const optionIndexInSelection = newSelection.indexOf(optionIndex);
if (optionIndexInSelection > -1) {
newSelection.splice(optionIndexInSelection, 1);
} else {
newSelection.push(optionIndex);
}
newSelection.sort((a, b) => a - b);
newAnswers[currentQuestionIndex] = { selectedOption: newSelection.length > 0 ? newSelection : null };
} else { // MCQ
newAnswers[currentQuestionIndex] = { selectedOption: [optionIndex] };
}
setAnswers(newAnswers);
};
const handleClearSelection = () => {
const newAnswers = [...answers];
newAnswers[currentQuestionIndex] = { selectedOption: null };
setAnswers(newAnswers);
};
const goToNext = () => {
if (currentQuestionIndex < quiz.questions.length - 1) {
setCurrentQuestionIndex(prev => prev + 1);
}
};
const goToPrevious = () => {
if (currentQuestionIndex > 0) {
setCurrentQuestionIndex(prev => prev - 1);
}
};
const currentQuestion = quiz.questions[currentQuestionIndex];
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
const progress = ((currentQuestionIndex + 1) / quiz.questions.length) * 100;
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Main Content */}
<div className="lg:col-span-8">
<div className="bg-white dark:bg-gray-800 p-6 md:p-8 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">
{/* Header */}
<div className="mb-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold text-gray-700 dark:text-gray-300">
Question {currentQuestionIndex + 1} <span className="text-gray-400 dark:text-gray-500">/ {quiz.questions.length}</span>
</h3>
<div className="flex items-center gap-3">
<div className={`text-xl font-semibold px-4 py-2 rounded-lg ${timeLeft < 60 ? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300' : 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'}`}>
Time: {minutes}:{seconds < 10 ? `0${seconds}` : seconds}
</div>
<button
onClick={handleQuit}
className="px-3 py-2 text-sm font-medium text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 hover:bg-red-100 dark:hover:bg-red-900/50 rounded-lg transition-colors border border-red-200 dark:border-red-800"
>
Quit
</button>
</div>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${progress}%` }}></div>
</div>
</div>
<Question
question={currentQuestion}
questionNumber={currentQuestionIndex + 1}
totalQuestions={quiz.questions.length}
/>
<div className="mt-6 space-y-3">
{currentQuestion.options.map((option, index) => {
const isSelected = Array.isArray(answers[currentQuestionIndex].selectedOption) && answers[currentQuestionIndex].selectedOption.includes(index);
return (
<label key={index} className={`flex items-center p-4 rounded-lg border-2 cursor-pointer transition-all duration-200 ${isSelected ? 'bg-blue-50 dark:bg-blue-900/30 border-blue-500 dark:border-blue-500 shadow-md' : 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/60'}`}>
<input
type={currentQuestion.questionType === 'MSQ' ? 'checkbox' : 'radio'}
name={`question-${currentQuestionIndex}`}
checked={isSelected}
onChange={() => handleAnswerSelect(index)}
className={`h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-400 ${currentQuestion.questionType === 'MSQ' ? 'rounded' : ''}`}
/>
<span className="ml-4 text-gray-700 dark:text-gray-200 text-lg">{option}</span>
</label>
)
})}
</div>
<div className="mt-6 flex justify-between items-center">
<button
onClick={handleClearSelection}
disabled={answers[currentQuestionIndex].selectedOption === null}
className="px-4 py-2 text-base font-semibold text-gray-600 dark:text-gray-400 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
Clear Selection
</button>
<div className="flex space-x-4">
<button onClick={goToPrevious} disabled={currentQuestionIndex === 0} className="px-8 py-3 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 font-semibold rounded-lg shadow-md hover:bg-gray-100 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition text-lg">
Previous
</button>
{currentQuestionIndex === quiz.questions.length - 1 ? (
<button onClick={handleSubmit} className="px-10 py-3 bg-green-600 text-white font-bold rounded-lg shadow-lg hover:bg-green-700 transition-transform hover:scale-105 text-lg">
Submit Quiz
</button>
) : (
<button onClick={goToNext} className="px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 disabled:opacity-50 transition text-lg">
Save & Next
</button>
)}
</div>
</div>
</div>
</div>
{/* Right Sidebar */}
<div className="lg:col-span-4 space-y-6">
<QuestionPalette
count={quiz.questions.length}
currentIndex={currentQuestionIndex}
answers={answers}
onQuestionSelect={setCurrentQuestionIndex}
/>
<Calculator />
</div>
</div>
);
};
export default QuizScreen;