mirror of
https://github.com/R0m1k3/Reviflow.git
synced 2026-10-24 07:28:08 +02:00
feat(quiz): implement two-step answer validation logic
This commit is contained in:
1 parent
6c43282d6f
commit
6531e3318d
2 files changed
+93
-47
No files matched your search
@@ -10,6 +10,7 @@ interface QuestionCardProps {
|
|||||||
currentStep: number;
|
currentStep: number;
|
||||||
totalSteps: number;
|
totalSteps: number;
|
||||||
explanation?: string;
|
explanation?: string;
|
||||||
|
isValidated: boolean; // New prop
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuestionCard({
|
export function QuestionCard({
|
||||||
@@ -20,24 +21,22 @@ export function QuestionCard({
|
|||||||
onSelectOption,
|
onSelectOption,
|
||||||
currentStep,
|
currentStep,
|
||||||
totalSteps,
|
totalSteps,
|
||||||
explanation
|
explanation,
|
||||||
|
isValidated // Destructure new prop
|
||||||
}: QuestionCardProps) {
|
}: QuestionCardProps) {
|
||||||
const [animation, setAnimation] = useState<'pop' | 'shake' | null>(null);
|
const [animation, setAnimation] = useState<'pop' | 'shake' | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedOption !== null) {
|
if (isValidated && selectedOption !== null) {
|
||||||
if (selectedOption === correctAnswer) {
|
if (selectedOption === correctAnswer) {
|
||||||
setAnimation('pop');
|
setAnimation('pop');
|
||||||
} else {
|
} else {
|
||||||
setAnimation('shake');
|
setAnimation('shake');
|
||||||
}
|
}
|
||||||
// Removed auto-hide timer for animation to keep state clear,
|
|
||||||
// but we might want to keep the effect.
|
|
||||||
// The user wants "time to read", so we shouldn't auto-advance too fast in parent.
|
|
||||||
const timer = setTimeout(() => setAnimation(null), 500);
|
const timer = setTimeout(() => setAnimation(null), 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [selectedOption, correctAnswer]);
|
}, [isValidated, selectedOption, correctAnswer]); // Trigger on validation
|
||||||
|
|
||||||
const isCorrect = selectedOption === correctAnswer;
|
const isCorrect = selectedOption === correctAnswer;
|
||||||
|
|
||||||
@@ -60,42 +59,69 @@ export function QuestionCard({
|
|||||||
<h2 className="text-3xl font-black text-slate-900 dark:text-white mb-10 leading-tight tracking-tight">{question}</h2>
|
<h2 className="text-3xl font-black text-slate-900 dark:text-white mb-10 leading-tight tracking-tight">{question}</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 mb-4">
|
<div className="grid grid-cols-1 gap-4 mb-4">
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => {
|
||||||
<button
|
// Logic for visual states
|
||||||
key={index}
|
let borderClass = 'border-slate-100 dark:border-slate-700';
|
||||||
disabled={selectedOption !== null}
|
let bgClass = 'hover:bg-indigo-50/30 dark:hover:bg-indigo-900/20';
|
||||||
onClick={() => onSelectOption(index)}
|
let textClass = '';
|
||||||
className={`w-full text-left p-5 border-2 rounded-xl transition-all duration-200 group relative ${selectedOption === null
|
let opacityClass = '';
|
||||||
? 'border-slate-100 dark:border-slate-700 hover:border-soft-indigo dark:hover:border-indigo-500 hover:bg-indigo-50/30 dark:hover:bg-indigo-900/20'
|
|
||||||
: selectedOption === index
|
if (selectedOption === index) {
|
||||||
? index === correctAnswer
|
if (!isValidated) {
|
||||||
? 'border-green-500 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400'
|
// Selected but not validated: Blue/Neutral
|
||||||
: 'border-orange-400 bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-400'
|
borderClass = 'border-indigo-500 dark:border-indigo-400';
|
||||||
: index === correctAnswer
|
bgClass = 'bg-indigo-50 dark:bg-indigo-900/20';
|
||||||
? 'border-green-500 bg-green-50/50 dark:bg-green-900/10'
|
textClass = 'text-indigo-700 dark:text-indigo-300';
|
||||||
: 'border-slate-100 dark:border-slate-700 opacity-50'
|
} else {
|
||||||
}`}
|
// Validated: Green or Red
|
||||||
>
|
if (index === correctAnswer) {
|
||||||
<div className="flex items-center">
|
borderClass = 'border-green-500';
|
||||||
<span className={`flex-shrink-0 h-8 w-8 flex items-center justify-center rounded-full border-2 text-sm font-bold transition-colors ${selectedOption === null
|
bgClass = 'bg-green-50 dark:bg-green-900/20';
|
||||||
? 'border-slate-200 dark:border-slate-600 text-slate-400 dark:text-slate-500 group-hover:border-soft-indigo dark:group-hover:border-indigo-400 group-hover:text-soft-indigo dark:group-hover:text-indigo-400'
|
textClass = 'text-green-700 dark:text-green-400';
|
||||||
: selectedOption === index
|
} else {
|
||||||
? index === correctAnswer
|
borderClass = 'border-orange-400';
|
||||||
? 'border-green-500 bg-green-500 text-white'
|
bgClass = 'bg-orange-50 dark:bg-orange-900/20';
|
||||||
: 'border-orange-400 bg-orange-400 text-white'
|
textClass = 'text-orange-700 dark:text-orange-400';
|
||||||
: index === correctAnswer
|
}
|
||||||
? 'border-green-500 text-green-500'
|
}
|
||||||
: 'border-slate-200 dark:border-slate-600 text-slate-300 dark:text-slate-600'
|
} else if (isValidated && index === correctAnswer) {
|
||||||
}`}>
|
// Show correct answer if user got it wrong
|
||||||
{String.fromCharCode(65 + index)}
|
borderClass = 'border-green-500';
|
||||||
</span>
|
bgClass = 'bg-green-50/50 dark:bg-green-900/10';
|
||||||
<span className="ml-4 text-lg font-medium text-slate-700 dark:text-slate-200">{option}</span>
|
textClass = 'text-green-700 dark:text-green-400';
|
||||||
</div>
|
} else if (isValidated) {
|
||||||
</button>
|
// Fade out other options when validated
|
||||||
))}
|
opacityClass = 'opacity-50';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
disabled={isValidated} // Disable buttons after validation
|
||||||
|
onClick={() => onSelectOption(index)}
|
||||||
|
className={`w-full text-left p-5 border-2 rounded-xl transition-all duration-200 group relative ${borderClass} ${bgClass} ${textClass} ${opacityClass}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className={`flex-shrink-0 h-8 w-8 flex items-center justify-center rounded-full border-2 text-sm font-bold transition-colors ${selectedOption === index
|
||||||
|
? isValidated
|
||||||
|
? index === correctAnswer
|
||||||
|
? 'border-green-500 bg-green-500 text-white'
|
||||||
|
: 'border-orange-400 bg-orange-400 text-white'
|
||||||
|
: 'border-indigo-500 bg-indigo-500 text-white' // Selected pre-validation
|
||||||
|
: isValidated && index === correctAnswer
|
||||||
|
? 'border-green-500 text-green-500' // Show correct
|
||||||
|
: 'border-slate-200 dark:border-slate-600 text-slate-400 dark:text-slate-500 group-hover:border-soft-indigo dark:group-hover:border-indigo-400 group-hover:text-soft-indigo dark:group-hover:text-indigo-400'
|
||||||
|
}`}>
|
||||||
|
{String.fromCharCode(65 + index)}
|
||||||
|
</span>
|
||||||
|
<span className={`ml-4 text-lg font-medium ${textClass || 'text-slate-700 dark:text-slate-200'}`}>{option}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedOption !== null && (
|
{isValidated && selectedOption !== null && (
|
||||||
<div className={`mt-6 p-6 rounded-2xl animate-in fade-in slide-in-from-top-2 duration-300 ${isCorrect ? 'bg-green-50 dark:bg-green-900/20 border border-green-100 dark:border-green-900/30' : 'bg-orange-50 dark:bg-orange-900/20 border border-orange-100 dark:border-orange-900/30'}`}>
|
<div className={`mt-6 p-6 rounded-2xl animate-in fade-in slide-in-from-top-2 duration-300 ${isCorrect ? 'bg-green-50 dark:bg-green-900/20 border border-green-100 dark:border-green-900/30' : 'bg-orange-50 dark:bg-orange-900/20 border border-orange-100 dark:border-orange-900/30'}`}>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className={`text-2xl ${isCorrect ? 'grayscale-0' : 'grayscale-0'}`}>
|
<div className={`text-2xl ${isCorrect ? 'grayscale-0' : 'grayscale-0'}`}>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function QuizPage() {
|
|||||||
const [quizData, setQuizData] = useState<QuizResponse | null>(null);
|
const [quizData, setQuizData] = useState<QuizResponse | null>(null);
|
||||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||||
const [selectedOption, setSelectedOption] = useState<number | null>(null);
|
const [selectedOption, setSelectedOption] = useState<number | null>(null);
|
||||||
|
const [isValidated, setIsValidated] = useState(false); // New state for validation
|
||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
const [answers, setAnswers] = useState<any[]>([]); // Track details
|
const [answers, setAnswers] = useState<any[]>([]); // Track details
|
||||||
const [isFinished, setIsFinished] = useState(false);
|
const [isFinished, setIsFinished] = useState(false);
|
||||||
@@ -129,19 +130,26 @@ export function QuizPage() {
|
|||||||
if (currentQuestionIndex < (quizData?.questions.length || 0) - 1) {
|
if (currentQuestionIndex < (quizData?.questions.length || 0) - 1) {
|
||||||
setCurrentQuestionIndex(prev => prev + 1);
|
setCurrentQuestionIndex(prev => prev + 1);
|
||||||
setSelectedOption(null);
|
setSelectedOption(null);
|
||||||
|
setIsValidated(false); // Reset validation for next question
|
||||||
} else {
|
} else {
|
||||||
setIsFinished(true);
|
setIsFinished(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Change: Selected option just updates state, doesn't lock yet
|
||||||
const handleOptionSelect = (index: number) => {
|
const handleOptionSelect = (index: number) => {
|
||||||
if (selectedOption !== null) return; // Prevent changing answer
|
if (isValidated) return; // Prevent changing answer if already validated
|
||||||
setSelectedOption(index);
|
setSelectedOption(index);
|
||||||
|
};
|
||||||
|
|
||||||
if (!quizData) return;
|
// New: Validate the answer
|
||||||
|
const handleValidate = () => {
|
||||||
|
if (selectedOption === null || isValidated || !quizData) return;
|
||||||
|
|
||||||
|
setIsValidated(true);
|
||||||
|
|
||||||
const currentQ = quizData.questions[currentQuestionIndex];
|
const currentQ = quizData.questions[currentQuestionIndex];
|
||||||
const isCorrect = index === currentQ.correct_answer;
|
const isCorrect = selectedOption === currentQ.correct_answer;
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
setScore(prev => prev + 1);
|
setScore(prev => prev + 1);
|
||||||
@@ -150,7 +158,7 @@ export function QuizPage() {
|
|||||||
// Record answer details
|
// Record answer details
|
||||||
setAnswers(prev => [...prev, {
|
setAnswers(prev => [...prev, {
|
||||||
question: currentQ.question,
|
question: currentQ.question,
|
||||||
user_answer: currentQ.options[index],
|
user_answer: currentQ.options[selectedOption],
|
||||||
correct_answer: currentQ.options[currentQ.correct_answer],
|
correct_answer: currentQ.options[currentQ.correct_answer],
|
||||||
is_correct: isCorrect,
|
is_correct: isCorrect,
|
||||||
original_content: null
|
original_content: null
|
||||||
@@ -311,13 +319,25 @@ export function QuizPage() {
|
|||||||
currentStep={currentQuestionIndex + 1}
|
currentStep={currentQuestionIndex + 1}
|
||||||
totalSteps={quizData.questions.length}
|
totalSteps={quizData.questions.length}
|
||||||
explanation={quizData.questions[currentQuestionIndex].explanation}
|
explanation={quizData.questions[currentQuestionIndex].explanation}
|
||||||
|
isValidated={isValidated} // Pass validation state
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* NEXT BUTTON */}
|
{/* VALIDATE BUTTON */}
|
||||||
{selectedOption !== null && (
|
{selectedOption !== null && !isValidated && (
|
||||||
|
<button
|
||||||
|
onClick={handleValidate}
|
||||||
|
className="mt-8 bg-indigo-600 text-white font-black text-lg py-4 px-12 rounded-2xl shadow-xl shadow-indigo-200 dark:shadow-none hover:bg-indigo-700 hover:scale-105 hover:shadow-2xl transition-all duration-300 animate-in fade-in slide-in-from-bottom-4 flex items-center gap-3 group"
|
||||||
|
>
|
||||||
|
<span>Valider la réponse</span>
|
||||||
|
<span className="text-2xl group-hover:scale-110 transition-transform">✓</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* NEXT BUTTON (Only show after validation) */}
|
||||||
|
{isValidated && (
|
||||||
<button
|
<button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
className="mt-8 bg-indigo-600 text-white font-black text-lg py-4 px-12 rounded-2xl shadow-xl shadow-indigo-200 hover:bg-indigo-700 hover:scale-105 hover:shadow-2xl transition-all duration-300 animate-in fade-in slide-in-from-bottom-4 flex items-center gap-3 group"
|
className="mt-8 bg-indigo-600 text-white font-black text-lg py-4 px-12 rounded-2xl shadow-xl shadow-indigo-200 dark:shadow-none hover:bg-indigo-700 hover:scale-105 hover:shadow-2xl transition-all duration-300 animate-in fade-in slide-in-from-bottom-4 flex items-center gap-3 group"
|
||||||
>
|
>
|
||||||
<span>Question Suivante</span>
|
<span>Question Suivante</span>
|
||||||
<span className="text-2xl group-hover:translate-x-1 transition-transform">➜</span>
|
<span className="text-2xl group-hover:translate-x-1 transition-transform">➜</span>
|
||||||
|
|||||||
Reference in new issue
Block a user