sno-quiz/frontend/src/pages/QuizPage.tsx
2025-09-17 22:22:14 +03:00

282 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Button,
CircularProgress,
Alert,
} from '@mui/material';
import { useNavigate, useParams } from 'react-router-dom';
import { apiService } from '../services/api';
import { useAuth } from '../context/AuthContext';
import { QuestionCard } from '../components/ui/QuestionCard';
import { AnswerOption } from '../components/ui/AnswerOption';
import type { Quiz, Question, UserAnswer } from '../types';
export const QuizPage: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const { user, updateUser } = useAuth();
const [quiz, setQuiz] = useState<Quiz | null>(null);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [answers, setAnswers] = useState<UserAnswer[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchQuiz = async () => {
if (!id) return;
try {
const response = await apiService.getQuizById(parseInt(id));
console.log('Quiz response:', response);
if (response.success && response.data) {
setQuiz(response.data);
// Initialize answers
const initialAnswers = response.data.questions?.map((q: Question) => ({
question_id: q.id,
option_ids: [],
})) || [];
console.log('Initial answers:', initialAnswers);
setAnswers(initialAnswers);
} else {
setError('Не удалось загрузить викторину');
}
} catch (err) {
console.error('Error fetching quiz:', err);
setError('Произошла ошибка при загрузке викторины');
} finally {
setLoading(false);
}
};
fetchQuiz();
}, [id]);
const handleAnswerChange = (questionId: number, optionId: string, isMultiple: boolean) => {
console.log('handleAnswerChange called:', { questionId, optionId, isMultiple });
setAnswers(prev => {
// Create deep copy to ensure immutability
const newAnswers = prev.map(answer => ({
...answer,
option_ids: [...answer.option_ids]
}));
const answerIndex = newAnswers.findIndex(a => a.question_id === questionId);
console.log('Current answers:', prev);
console.log('Answer index:', answerIndex);
if (answerIndex === -1) return prev;
const optionIdNum = parseInt(optionId);
if (isMultiple) {
const currentOptions = newAnswers[answerIndex].option_ids;
const optionIndex = currentOptions.indexOf(optionIdNum);
if (optionIndex === -1) {
currentOptions.push(optionIdNum);
} else {
currentOptions.splice(optionIndex, 1);
}
} else {
newAnswers[answerIndex].option_ids = [optionIdNum];
}
console.log('Updated answers:', newAnswers);
return newAnswers;
});
};
const handleNext = () => {
if (quiz && currentQuestionIndex < quiz.questions!.length - 1) {
setCurrentQuestionIndex(prev => prev + 1);
}
};
const handlePrevious = () => {
if (currentQuestionIndex > 0) {
setCurrentQuestionIndex(prev => prev - 1);
}
};
const handleSubmit = async () => {
if (!quiz || !id) return;
setSubmitting(true);
setError(null);
try {
const response = await apiService.submitQuiz(parseInt(id), { answers });
if (response.success && response.data) {
// Update user balance with earned stars
if (response.data.stars_earned > 0) {
updateUser({
stars_balance: (user?.stars_balance || 0) + response.data.stars_earned
});
}
// Navigate to results page
navigate('/quiz-result', {
state: {
result: response.data,
quizTitle: quiz.title
}
});
} else {
setError('Не удалось отправить ответы');
}
} catch (err) {
console.error('Error submitting quiz:', err);
setError('Произошла ошибка при отправке ответов');
} finally {
setSubmitting(false);
}
};
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<CircularProgress sx={{ color: '#FFD700' }} />
</Box>
);
}
if (error || !quiz) {
return (
<Alert
severity="error"
sx={{
mt: 4,
backgroundColor: 'rgba(244, 67, 54, 0.1)',
color: '#ffffff',
border: '1px solid #f44336',
}}
>
{error || 'Викторина не найдена'}
</Alert>
);
}
const currentQuestion = quiz.questions?.[currentQuestionIndex];
const currentAnswer = answers.find(a => a.question_id === currentQuestion?.id);
const isLastQuestion = currentQuestionIndex === quiz.questions!.length - 1;
const canProceed = currentAnswer?.option_ids && currentAnswer.option_ids.length > 0;
console.log('Current state:', {
currentQuestion,
currentAnswer,
answers,
canProceed
});
return (
<Box>
{/* Header */}
<Box sx={{ mb: 4 }}>
<Typography
variant="h4"
sx={{
color: '#FFD700',
fontWeight: 'bold',
mb: 1,
}}
>
{quiz.title}
</Typography>
<Typography
variant="body1"
sx={{
color: '#888',
}}
>
Вопрос {currentQuestionIndex + 1} из {quiz.questions?.length}
</Typography>
</Box>
{/* Question Card */}
<QuestionCard
question={currentQuestion?.text || ''}
questionNumber={currentQuestionIndex + 1}
totalQuestions={quiz.questions?.length || 0}
/>
{/* Answer Options */}
<Box sx={{ mt: 3, pointerEvents: 'auto' }}>
{currentQuestion?.options.map((option) => (
<AnswerOption
key={option.id}
id={option.id.toString()}
text={option.text}
type={currentQuestion.type}
isSelected={currentAnswer?.option_ids.includes(option.id) || false}
onSelect={(optionId) => {
console.log('AnswerOption onSelect called:', optionId);
if (currentQuestion) {
handleAnswerChange(currentQuestion.id, optionId, currentQuestion.type === 'multiple');
}
}}
/>
))}
</Box>
{/* Navigation Buttons */}
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'space-between' }}>
<Button
variant="outlined"
onClick={handlePrevious}
disabled={currentQuestionIndex === 0}
sx={{
borderColor: '#333',
color: '#ffffff',
'&:hover': {
borderColor: '#FFD700',
color: '#FFD700',
},
}}
>
Назад
</Button>
<Box sx={{ display: 'flex', gap: 2 }}>
{!isLastQuestion ? (
<Button
variant="contained"
onClick={handleNext}
disabled={!canProceed}
sx={{
backgroundColor: '#FFD700',
color: '#000000',
'&:hover': {
backgroundColor: '#FFC700',
},
}}
>
Далее
</Button>
) : (
<Button
variant="contained"
onClick={handleSubmit}
disabled={!canProceed || submitting}
sx={{
backgroundColor: '#4CAF50',
color: '#ffffff',
'&:hover': {
backgroundColor: '#45a049',
},
}}
>
{submitting ? <CircularProgress size={20} /> : 'Завершить'}
</Button>
)}
</Box>
</Box>
</Box>
);
};