/
obuxxxov
/
CodeForge
Обзор
Документация
Войти
/
obuxxxov
/
CodeForge
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
comb
visual/src/Test.tsx
1 207 строк
42 KB
ObliInSe
обновляются рекомендации
19 авг 2025, 01:12
19 авг 2025, 01:12
48808c4
Код
Авторство
О чём код?
import React, { useState, useCallback, useEffect, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useDataRefresh } from './DataRefreshContext'; // Базовый интерфейс для всех типов вопросов interface BaseQuestion { id: number; type: QuestionType; text: string; points: number; explanation?: string; } // Типы вопросов type QuestionType = | 'multiple_choice' // Множественный выбор (один ответ) | 'multiple_select' // Множественный выбор (несколько ответов) | 'true_false' // Верно/неверно | 'code_output' // Что выведет код? | 'find_bug' // Найди ошибку | 'fill_code' // Заполни пропуски в коде | 'ordering' // Упорядочивание | 'scenario' // Сценарная задача | 'drag_drop'; // Перетаскивание // Множественный выбор (один ответ) interface MultipleChoiceQuestion extends BaseQuestion { type: 'multiple_choice'; options: string[]; correctAnswer: number; } // Множественный выбор (несколько ответов) interface MultipleSelectQuestion extends BaseQuestion { type: 'multiple_select'; options: string[]; correctAnswers: number[]; } // Верно/неверно interface TrueFalseQuestion extends BaseQuestion { type: 'true_false'; correctAnswer: boolean; } // Анализ кода interface CodeOutputQuestion extends BaseQuestion { type: 'code_output'; code: string; language: string; options: string[]; correctAnswer: number; } // Поиск ошибки interface FindBugQuestion extends BaseQuestion { type: 'find_bug'; code: string; language: string; options: string[]; correctAnswer: number; bugLocation?: string; } // Заполнение пропусков interface FillCodeQuestion extends BaseQuestion { type: 'fill_code'; codeTemplate: string; language: string; blanks: { id: string; options: string[]; correctAnswer: number }[]; } // Упорядочивание interface OrderingQuestion extends BaseQuestion { type: 'ordering'; items: { id: string; text: string }[]; correctOrder: string[]; } // Сценарий interface ScenarioQuestion extends BaseQuestion { type: 'scenario'; scenario: string; options: string[]; correctAnswer: number; } // Перетаскивание interface DragDropQuestion extends BaseQuestion { type: 'drag_drop'; instruction: string; draggableItems: { id: string; text: string }[]; dropZones: { id: string; label: string; acceptsItems: string[] }[]; correctPlacements: { item: string; zone: string }[]; } // Объединенный тип type Question = | MultipleChoiceQuestion | MultipleSelectQuestion | TrueFalseQuestion | CodeOutputQuestion | FindBugQuestion | FillCodeQuestion | OrderingQuestion | ScenarioQuestion | DragDropQuestion; interface TestResult { score: number; totalQuestions: number; totalPoints: number; earnedPoints: number; passed: boolean; detailedResults: QuestionResult[]; } interface QuestionResult { questionId: number; questionType: QuestionType; isCorrect: boolean; earnedPoints: number; maxPoints: number; userAnswer: any; correctAnswer: any; } const Test: React.FC = () => { const [params] = useSearchParams(); const navigate = useNavigate(); const { refreshData } = useDataRefresh(); const block = params.get('block') || ''; const topic = params.get('topic') || ''; const [questions, setQuestions] = useState<Question[]>([]); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [userAnswers, setUserAnswers] = useState<Record<number, any>>({}); const [testFinished, setTestFinished] = useState(false); const [testResult, setTestResult] = useState<TestResult | null>(null); const [loading, setLoading] = useState(true); const userAnswersRef = useRef<Record<number, any>>({}); useEffect(() => { userAnswersRef.current = userAnswers; }, [userAnswers]); // Генерация тестовых вопросов (в реальном приложении будут загружаться с сервера) useEffect(() => { const generateQuestions = () => { const sampleQuestions: Question[] = [ // 1. Множественный выбор (один ответ) { id: 1, type: 'multiple_choice', text: `Какой основной принцип применяется в теме "${topic}"?`, points: 10, options: [ 'Принцип единственной ответственности', 'Принцип открытости/закрытости', 'Принцип подстановки Лисков', 'Принцип инверсии зависимостей' ], correctAnswer: 0, explanation: 'Принцип единственной ответственности является основой для большинства архитектурных решений.' }, // 2. Множественный выбор (несколько ответов) { id: 2, type: 'multiple_select', text: `Какие преимущества дает изучение "${topic}"? (Выберите все подходящие)`, points: 15, options: [ 'Улучшение структуры кода', 'Повышение производительности', 'Упрощение тестирования', 'Снижение технического долга' ], correctAnswers: [0, 2, 3], explanation: 'Изучение архитектурных принципов улучшает структуру, упрощает тестирование и снижает технический долг.' }, // 3. Верно/неверно { id: 3, type: 'true_false', text: 'Принцип DRY (Don\'t Repeat Yourself) означает, что нужно избегать дублирования кода.', points: 5, correctAnswer: true, explanation: 'DRY - это фундаментальный принцип программирования, направленный на устранение дублирования.' }, // 4. Анализ кода { id: 4, type: 'code_output', text: 'Что выведет следующий Python код?', code: `def fibonacci(n): if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(5))`, language: 'python', points: 20, options: ['3', '5', '8', '13'], correctAnswer: 1, explanation: 'Последовательность Фибоначчи: 0, 1, 1, 2, 3, 5, 8... fibonacci(5) = 5' }, // 5. Поиск ошибки { id: 5, type: 'find_bug', text: 'Найдите ошибку в этом коде:', code: `def divide_numbers(a, b): result = a / b return result numbers = [10, 5, 0, 2] for i in range(len(numbers)): print(divide_numbers(numbers[i], numbers[i+1]))`, language: 'python', points: 15, options: [ 'Деление на ноль', 'Выход за границы массива', 'Неправильный тип данных', 'Синтаксическая ошибка' ], correctAnswer: 1, explanation: 'Цикл обращается к numbers[i+1], что при i=3 даст numbers[4] - выход за границы массива.' }, // 6. Упорядочивание { id: 6, type: 'ordering', text: 'Упорядочьте этапы CI/CD процесса:', points: 15, items: [ { id: 'deploy', text: 'Деплой в продакшн' }, { id: 'test', text: 'Запуск тестов' }, { id: 'build', text: 'Сборка проекта' }, { id: 'commit', text: 'Коммит кода' } ], correctOrder: ['commit', 'build', 'test', 'deploy'], explanation: 'Классический CI/CD pipeline: коммит → сборка → тесты → деплой.' }, // 7. Сценарий { id: 7, type: 'scenario', text: 'У вас медленный SQL-запрос, который выбирает данные из большой таблицы.', scenario: 'Таблица содержит 10 миллионов записей. Запрос выполняется 30 секунд и использует WHERE по полю, которое не проиндексировано. Пользователи жалуются на медленную работу.', points: 25, options: [ 'Увеличить RAM сервера', 'Добавить индекс на поле в WHERE', 'Переписать запрос на NoSQL', 'Разделить таблицу на несколько' ], correctAnswer: 1, explanation: 'Добавление индекса на поле в WHERE условии даст наибольший прирост производительности.' } ]; setQuestions(sampleQuestions.slice(0, 1)); setLoading(false); }; generateQuestions(); }, [topic]); const handleAnswerSelect = useCallback((questionId: number, answer: any) => { setUserAnswers(prev => ({ ...prev, [questionId]: answer })); console.log('Selected answer:', { questionId, answer }); }, []); const handleMultipleSelect = useCallback((questionId: number, optionIndex: number) => { setUserAnswers(prev => { const currentAnswers = prev[questionId] || []; const newAnswers = currentAnswers.includes(optionIndex) ? currentAnswers.filter((i: number) => i !== optionIndex) : [...currentAnswers, optionIndex]; return { ...prev, [questionId]: newAnswers }; }); }, []); const handleOrdering = useCallback((questionId: number, orderedItems: string[]) => { setUserAnswers(prev => ({ ...prev, [questionId]: orderedItems })); }, []); const handleNextQuestion = useCallback(() => { if (currentQuestionIndex < questions.length - 1) { setCurrentQuestionIndex(prev => prev + 1); } else { finishTest(); } }, [currentQuestionIndex, questions.length]); const handlePrevQuestion = useCallback(() => { if (currentQuestionIndex > 0) { setCurrentQuestionIndex(prev => prev - 1); } }, [currentQuestionIndex]); const finishTest = useCallback(async () => { let totalPoints = 0; let earnedPoints = 0; const detailedResults: QuestionResult[] = []; const snapshot = userAnswersRef.current; questions.forEach(question => { totalPoints += question.points; const userAnswer = snapshot[question.id]; let isCorrect = false; let earnedQuestionPoints = 0; // Проверка правильности ответа в зависимости от типа вопроса switch (question.type) { case 'multiple_choice': case 'code_output': case 'find_bug': case 'scenario': isCorrect = Number(userAnswer) === Number((question as any).correctAnswer); break; case 'true_false': isCorrect = Boolean(userAnswer) === Boolean((question as any).correctAnswer); break; case 'multiple_select': const correctAnswers = question.correctAnswers.sort(); const userAnswersSorted = (userAnswer || []).sort(); isCorrect = JSON.stringify(correctAnswers) === JSON.stringify(userAnswersSorted); break; case 'ordering': const correctOrder = question.correctOrder; const userOrder = userAnswer || []; isCorrect = JSON.stringify(correctOrder) === JSON.stringify(userOrder); break; default: isCorrect = false; } if (isCorrect) { earnedQuestionPoints = question.points; earnedPoints += question.points; } console.log('Test eval:', { id: question.id, type: question.type, userAnswer, correctAnswer: (question as any).correctAnswer ?? (question as any).correctAnswers ?? (question as any).correctOrder, isCorrect }); detailedResults.push({ questionId: question.id, questionType: question.type, isCorrect, earnedPoints: earnedQuestionPoints, maxPoints: question.points, userAnswer, correctAnswer: getCorrectAnswer(question) }); }); const score = Math.round((earnedPoints / totalPoints) * 100); const passed = score >= 70; // Проходной балл 70% setTestResult({ score, totalQuestions: questions.length, totalPoints, earnedPoints, passed, detailedResults }); setTestFinished(true); // Если успешно пройдено, отмечаем тему как пройденную в matrix2.xlsx if (passed && topic) { try { // 1. Отмечаем тему как пройденную const completionResponse = await fetch('http://localhost:8000/api/mark_topic_completion', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ block, topic, completed: true }) }); if (completionResponse.ok) { const completionResult = await completionResponse.json(); console.log('Marked topic completion:', completionResult); // 2. Обновляем AI рекомендации const recommendationsResponse = await fetch('http://localhost:8000/api/update_ai_recommendations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ block, topic, completed: true }) }); if (recommendationsResponse.ok) { const recommendationsResult = await recommendationsResponse.json(); console.log('Updated AI recommendations:', recommendationsResult); // 3. Обновляем данные в Details компоненте if (refreshData) { console.log('🔄 Вызываем обновление данных в Details...'); await refreshData(); } else { console.log('⚠️ Функция обновления данных не найдена, возможно Details не загружен'); } } else { console.error('Failed to update AI recommendations:', await recommendationsResponse.text()); } } else { console.error('Failed to mark topic completion:', await completionResponse.text()); } } catch (e) { console.error('Mark completion exception:', e); } } }, [questions, topic, block, refreshData]); const getCorrectAnswer = (question: Question): any => { switch (question.type) { case 'multiple_choice': case 'code_output': case 'find_bug': case 'scenario': return question.correctAnswer; case 'true_false': return question.correctAnswer; case 'multiple_select': return question.correctAnswers; case 'ordering': return question.correctOrder; default: return null; } }; const handleReturnToDetails = useCallback(() => { navigate(`/details?block=${encodeURIComponent(block)}`); }, [navigate, block]); const handleRetakeTest = useCallback(() => { setUserAnswers({}); setCurrentQuestionIndex(0); setTestFinished(false); setTestResult(null); }, []); if (loading) { return ( <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e40af 100%)', color: '#fff' }}> <div style={{ fontSize: 18 }}>Загрузка теста...</div> </div> ); } if (testFinished && testResult) { return ( <div style={{ minHeight: '100vh', background: 'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e40af 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' }}> <div style={{ width: '100%', maxWidth: 600, background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(6, 214, 160, 0.05))', border: '2px solid rgba(168, 85, 247, 0.3)', borderRadius: 20, padding: 40, textAlign: 'center', boxShadow: '0 25px 60px rgba(0,0,0,0.4), 0 0 40px rgba(168, 85, 247, 0.2)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)', color: '#E6F1FF' }}> <h1 style={{ margin: '0 0 24px 0', fontSize: 32, fontWeight: 700, background: 'linear-gradient(135deg, #69EBD0, #a855f7)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}> Тест завершен! </h1> <div style={{ fontSize: 24, fontWeight: 600, margin: '24px 0', color: testResult.passed ? '#69EBD0' : '#fca5a5' }}> Ваш результат: {testResult.score}% </div> <div style={{ fontSize: 18, margin: '16px 0', opacity: 0.9 }}> Набрано баллов: {testResult.earnedPoints} из {testResult.totalPoints} </div> <div style={{ fontSize: 16, margin: '16px 0', opacity: 0.8 }}> Правильных ответов: {testResult.detailedResults.filter(r => r.isCorrect).length} из {testResult.totalQuestions} </div> <div style={{ padding: '20px', margin: '24px 0', borderRadius: 12, background: testResult.passed ? 'rgba(105, 235, 208, 0.1)' : 'rgba(252, 165, 165, 0.1)', border: testResult.passed ? '1px solid rgba(105, 235, 208, 0.3)' : '1px solid rgba(252, 165, 165, 0.3)', fontSize: 16, lineHeight: '1.6' }}> {testResult.passed ? ( <> 🎉 <strong>Поздравляем!</strong><br /> Вы успешно прошли тест по теме "{topic}". Теперь вы можете перейти к изучению следующих тем. </> ) : ( <> 📚 <strong>Тест не пройден</strong><br /> Рекомендуем повторить материал по теме "{topic}" и попробовать снова. Для прохождения необходимо набрать минимум 70%. </> )} </div> <div style={{ display: 'flex', gap: 16, justifyContent: 'center', flexWrap: 'wrap', marginTop: 32 }}> <button onClick={handleRetakeTest} style={{ padding: '12px 24px', borderRadius: 12, border: '2px solid rgba(168, 85, 247, 0.6)', background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.25), rgba(59, 130, 246, 0.25))', color: '#ffffff', fontSize: 15, fontWeight: 600, cursor: 'pointer', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)' }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; e.currentTarget.style.boxShadow = '0 0 25px rgba(168, 85, 247, 0.5)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; }} > 🔄 Пройти заново </button> <button onClick={handleReturnToDetails} style={{ padding: '12px 24px', borderRadius: 12, border: '2px solid rgba(105, 235, 208, 0.6)', background: 'linear-gradient(135deg, rgba(105, 235, 208, 0.25), rgba(6, 214, 160, 0.25))', color: '#ffffff', fontSize: 15, fontWeight: 600, cursor: 'pointer', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)' }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; e.currentTarget.style.boxShadow = '0 0 25px rgba(105, 235, 208, 0.5)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; }} > ← Вернуться к материалам </button> </div> </div> </div> ); } const currentQuestion = questions[currentQuestionIndex]; const progress = ((currentQuestionIndex + 1) / questions.length) * 100; // Компонент для рендеринга разных типов вопросов const renderQuestion = (question: Question) => { const userAnswer = userAnswers[question.id]; switch (question.type) { case 'multiple_choice': case 'code_output': case 'find_bug': case 'scenario': return ( <> {/* Код для code_output и find_bug вопросов */} {(question.type === 'code_output' || question.type === 'find_bug') && ( <div style={{ background: 'rgba(0,0,0,0.4)', borderRadius: 12, padding: 20, marginBottom: 24, border: '1px solid rgba(105, 235, 208, 0.3)', position: 'relative' }}> {/* Заголовок с языком программирования */} <div style={{ position: 'absolute', top: 8, right: 12, background: 'rgba(105, 235, 208, 0.2)', borderRadius: 6, padding: '2px 8px', fontSize: 11, fontWeight: 600, color: '#69EBD0', textTransform: 'uppercase' }}> {question.language} </div> <pre style={{ margin: 0, color: '#E6F1FF', fontSize: 14, fontFamily: 'Monaco, Consolas, "Liberation Mono", monospace', lineHeight: 1.6, overflow: 'auto', textAlign: 'left', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}> <code style={{ display: 'block', textAlign: 'left' }}>{question.code}</code> </pre> </div> )} {/* Сценарий для scenario вопросов */} {question.type === 'scenario' && question.scenario && ( <div style={{ background: 'rgba(105, 235, 208, 0.1)', border: '1px solid rgba(105, 235, 208, 0.3)', borderRadius: 12, padding: 20, marginBottom: 24, fontSize: 15, lineHeight: 1.6 }}> <strong>Ситуация:</strong><br /> {question.scenario} </div> )} <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> {question.options.map((option, index) => ( <button key={index} onClick={() => handleAnswerSelect(question.id, index)} style={{ padding: '16px 20px', borderRadius: 12, border: userAnswer === index ? '2px solid rgba(105, 235, 208, 0.8)' : '2px solid rgba(168, 85, 247, 0.3)', background: userAnswer === index ? 'linear-gradient(135deg, rgba(105, 235, 208, 0.25), rgba(6, 214, 160, 0.15))' : 'rgba(0,0,0,0.2)', color: '#E6F1FF', fontSize: 16, textAlign: 'left', cursor: 'pointer', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)', boxShadow: userAnswer === index ? '0 0 20px rgba(105, 235, 208, 0.4)' : 'none' }} onMouseEnter={(e) => { if (userAnswer !== index) { e.currentTarget.style.background = 'rgba(168, 85, 247, 0.2)'; e.currentTarget.style.borderColor = 'rgba(168, 85, 247, 0.5)'; } }} onMouseLeave={(e) => { if (userAnswer !== index) { e.currentTarget.style.background = 'rgba(0,0,0,0.2)'; e.currentTarget.style.borderColor = 'rgba(168, 85, 247, 0.3)'; } }} > <span style={{ display: 'inline-block', width: 24, marginRight: 12, fontWeight: 600, color: userAnswer === index ? '#69EBD0' : '#a855f7' }}> {String.fromCharCode(65 + index)}. </span> {option} </button> ))} </div> </> ); case 'multiple_select': const selectedOptions = userAnswer || []; return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ background: 'rgba(105, 235, 208, 0.1)', border: '1px solid rgba(105, 235, 208, 0.3)', borderRadius: 8, padding: 12, fontSize: 14, textAlign: 'center' }}> 💡 Выберите все правильные варианты </div> {question.options.map((option, index) => ( <button key={index} onClick={() => handleMultipleSelect(question.id, index)} style={{ padding: '16px 20px', borderRadius: 12, border: selectedOptions.includes(index) ? '2px solid rgba(105, 235, 208, 0.8)' : '2px solid rgba(168, 85, 247, 0.3)', background: selectedOptions.includes(index) ? 'linear-gradient(135deg, rgba(105, 235, 208, 0.25), rgba(6, 214, 160, 0.15))' : 'rgba(0,0,0,0.2)', color: '#E6F1FF', fontSize: 16, textAlign: 'left', cursor: 'pointer', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)', boxShadow: selectedOptions.includes(index) ? '0 0 20px rgba(105, 235, 208, 0.4)' : 'none' }} > <span style={{ display: 'inline-block', width: 24, marginRight: 12, fontWeight: 600, color: selectedOptions.includes(index) ? '#69EBD0' : '#a855f7' }}> {selectedOptions.includes(index) ? '✓' : '○'} </span> {option} </button> ))} </div> ); case 'true_false': return ( <div style={{ display: 'flex', gap: 24, justifyContent: 'center' }}> {[true, false].map((value) => ( <button key={String(value)} onClick={() => handleAnswerSelect(question.id, value)} style={{ padding: '20px 40px', borderRadius: 16, border: userAnswer === value ? '2px solid rgba(105, 235, 208, 0.8)' : '2px solid rgba(168, 85, 247, 0.3)', background: userAnswer === value ? 'linear-gradient(135deg, rgba(105, 235, 208, 0.25), rgba(6, 214, 160, 0.15))' : 'rgba(0,0,0,0.2)', color: '#E6F1FF', fontSize: 18, fontWeight: 600, cursor: 'pointer', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)', minWidth: 120, boxShadow: userAnswer === value ? '0 0 20px rgba(105, 235, 208, 0.4)' : 'none' }} > {value ? '✅ Верно' : '❌ Неверно'} </button> ))} </div> ); case 'ordering': const orderedItems = userAnswer || question.items.map(i => i.id); return ( <div> <div style={{ background: 'rgba(105, 235, 208, 0.1)', border: '1px solid rgba(105, 235, 208, 0.3)', borderRadius: 8, padding: 12, marginBottom: 20, fontSize: 14, textAlign: 'center' }}> 🔄 Перетащите элементы в правильном порядке </div> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> {orderedItems.map((itemId: string, index: number) => { const item = question.items.find(i => i.id === itemId); return ( <div key={itemId} style={{ padding: '16px 20px', background: 'rgba(168, 85, 247, 0.2)', border: '2px solid rgba(168, 85, 247, 0.4)', borderRadius: 12, cursor: 'move', display: 'flex', alignItems: 'center', gap: 16 }} > <span style={{ background: 'rgba(105, 235, 208, 0.3)', borderRadius: '50%', width: 30, height: 30, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600 }}> {index + 1} </span> {item?.text} <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}> {index > 0 && ( <button onClick={() => { const newOrder = [...orderedItems]; [newOrder[index], newOrder[index - 1]] = [newOrder[index - 1], newOrder[index]]; handleOrdering(question.id, newOrder); }} style={{ background: 'rgba(105, 235, 208, 0.3)', border: 'none', borderRadius: 6, padding: '4px 8px', cursor: 'pointer', color: '#fff' }} > ↑ </button> )} {index < orderedItems.length - 1 && ( <button onClick={() => { const newOrder = [...orderedItems]; [newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]]; handleOrdering(question.id, newOrder); }} style={{ background: 'rgba(105, 235, 208, 0.3)', border: 'none', borderRadius: 6, padding: '4px 8px', cursor: 'pointer', color: '#fff' }} > ↓ </button> )} </div> </div> ); })} </div> </div> ); default: return <div>Неподдерживаемый тип вопроса</div>; } }; return ( <> {/* CSS анимации */} <style>{` @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.1); } } `}</style> <div style={{ minHeight: '100vh', background: 'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e40af 100%)', padding: '20px' }}> <div style={{ width: '100%', maxWidth: 800, margin: '0 auto' }}> {/* Header */} <div style={{ background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(6, 214, 160, 0.05))', border: '2px solid rgba(168, 85, 247, 0.3)', borderRadius: 20, padding: '24px', marginBottom: 24, boxShadow: '0 25px 60px rgba(0,0,0,0.4), 0 0 40px rgba(168, 85, 247, 0.2)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)', color: '#E6F1FF' }}> <h1 style={{ margin: '0 0 16px 0', fontSize: 28, fontWeight: 700, textAlign: 'center', background: 'linear-gradient(135deg, #69EBD0, #a855f7)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}> Тестирование по теме </h1> <div style={{ fontSize: 18, textAlign: 'center', marginBottom: 20, opacity: 0.9 }}> {topic} </div> {/* Progress Bar */} <div style={{ background: 'rgba(0,0,0,0.2)', borderRadius: 10, height: 8, marginBottom: 16 }}> <div style={{ background: 'linear-gradient(90deg, #69EBD0, #a855f7)', borderRadius: 10, height: '100%', width: `${progress}%`, transition: 'width 0.3s ease', boxShadow: '0 0 10px rgba(105, 235, 208, 0.5)' }} /> </div> <div style={{ textAlign: 'center', fontSize: 14, opacity: 0.8 }}> Вопрос {currentQuestionIndex + 1} из {questions.length} </div> </div> {/* Question Card */} <div style={{ background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(6, 214, 160, 0.05))', border: '2px solid rgba(168, 85, 247, 0.3)', borderRadius: 20, padding: '32px', marginBottom: 24, boxShadow: '0 25px 60px rgba(0,0,0,0.4), 0 0 40px rgba(168, 85, 247, 0.2)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)', color: '#E6F1FF' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}> <div style={{ background: 'rgba(105, 235, 208, 0.2)', borderRadius: 8, padding: '4px 12px', fontSize: 12, fontWeight: 600, border: '1px solid rgba(105, 235, 208, 0.4)' }}> {currentQuestion.points} баллов </div> <div style={{ background: 'rgba(168, 85, 247, 0.2)', borderRadius: 8, padding: '4px 12px', fontSize: 12, fontWeight: 600, border: '1px solid rgba(168, 85, 247, 0.4)', textTransform: 'capitalize' }}> {currentQuestion.type.replace('_', ' ')} </div> </div> <h2 style={{ margin: '0 0 32px 0', fontSize: 22, fontWeight: 600, lineHeight: '1.4' }}> {currentQuestion.text} </h2> {renderQuestion(currentQuestion)} </div> {/* Navigation */} <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}> <button onClick={handlePrevQuestion} disabled={currentQuestionIndex === 0} style={{ padding: '12px 24px', borderRadius: 12, border: '2px solid rgba(168, 85, 247, 0.6)', background: currentQuestionIndex === 0 ? 'rgba(0,0,0,0.2)' : 'linear-gradient(135deg, rgba(168, 85, 247, 0.25), rgba(59, 130, 246, 0.25))', color: currentQuestionIndex === 0 ? 'rgba(255,255,255,0.4)' : '#ffffff', fontSize: 15, fontWeight: 600, cursor: currentQuestionIndex === 0 ? 'not-allowed' : 'pointer', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)', opacity: currentQuestionIndex === 0 ? 0.5 : 1 }} onMouseEnter={(e) => { if (currentQuestionIndex !== 0) { e.currentTarget.style.transform = 'scale(1.05)'; e.currentTarget.style.boxShadow = '0 0 25px rgba(168, 85, 247, 0.5)'; } }} onMouseLeave={(e) => { if (currentQuestionIndex !== 0) { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; } }} > ← Предыдущий </button> <button onClick={handleReturnToDetails} style={{ padding: '12px 24px', borderRadius: 12, border: '2px solid rgba(252, 165, 165, 0.6)', background: 'linear-gradient(135deg, rgba(252, 165, 165, 0.25), rgba(239, 68, 68, 0.25))', color: '#ffffff', fontSize: 15, fontWeight: 600, cursor: 'pointer', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)' }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; e.currentTarget.style.boxShadow = '0 0 25px rgba(252, 165, 165, 0.5)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; }} > Прервать тест </button> <button onClick={handleNextQuestion} disabled={userAnswers[currentQuestion.id] === undefined} style={{ padding: '12px 24px', borderRadius: 12, border: '2px solid rgba(105, 235, 208, 0.6)', background: userAnswers[currentQuestion.id] !== undefined ? 'linear-gradient(135deg, rgba(105, 235, 208, 0.25), rgba(6, 214, 160, 0.25))' : 'rgba(0,0,0,0.2)', color: userAnswers[currentQuestion.id] !== undefined ? '#ffffff' : 'rgba(255,255,255,0.4)', fontSize: 15, fontWeight: 600, cursor: userAnswers[currentQuestion.id] !== undefined ? 'pointer' : 'not-allowed', transition: 'all 0.3s ease', backdropFilter: 'blur(8px)', opacity: userAnswers[currentQuestion.id] !== undefined ? 1 : 0.5 }} onMouseEnter={(e) => { if (userAnswers[currentQuestion.id] !== undefined) { e.currentTarget.style.transform = 'scale(1.05)'; e.currentTarget.style.boxShadow = '0 0 25px rgba(105, 235, 208, 0.5)'; } }} onMouseLeave={(e) => { if (userAnswers[currentQuestion.id] !== undefined) { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; } }} > {currentQuestionIndex === questions.length - 1 ? 'Завершить тест' : 'Следующий →'} </button> </div> </div> </div> </> ); }; export default Test;