282 lines
8.0 KiB
TypeScript
282 lines
8.0 KiB
TypeScript
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>
|
||
);
|
||
}; |