/
TimurIsm
/
OpenEyes
Обзор
Документация
Войти
/
TimurIsm
/
OpenEyes
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
front/src/components/AddTestQuestion.jsx
344 строки
11 KB
TimurIsm
first_commit
16 май 2026, 15:34
16 май 2026, 15:34
4d3f198
Код
Авторство
О чём код?
import React, { useState, useEffect, useRef } from 'react'; import { DeleteIcon } from '../buttons/IconButton'; import { deepTrim } from './stringUtils'; const MAX_CONTENT_LENGTH = 100; const MAX_ANSWER_LENGTH = 100; const MAX_OPTION_LENGTH = 100; const MAX_OPTIONS_COUNT = 7; const AddTestQuestion = ({ index, question, folder, handleQuestionChange, handleQuestionFileChange, handleRemoveQuestion, hasPathError }) => { const [newOption, setNewOption] = useState(''); const initializedRef = useRef(false); useEffect(() => { if (!initializedRef.current && question.data_type === 'select') { if (!question.answer_options || (Array.isArray(question.answer_options) && question.answer_options.length === 0)) { handleQuestionChange(index, 'answer_options', []); initializedRef.current = true; } } if (question.data_type === 'input') { initializedRef.current = false; } }, [question.data_type]); const validateImagePath = (path) => { if (!path || !folder) return true; const imageFolder = path.split('/').filter(Boolean)[0] || ''; const inputFolder = folder.replace(/^\/|\/$/g, ''); return imageFolder === inputFolder; }; const handleDataTypeChange = (e) => { const newType = e.target.value; initializedRef.current = false; if (newType === 'select') { handleQuestionChange(index, 'data_type', newType); setTimeout(() => { if (!question.answer_options || (Array.isArray(question.answer_options) && question.answer_options.length === 0)) { handleQuestionChange(index, 'answer_options', []); } handleQuestionChange(index, 'correct_answer', ''); }, 0); } else { handleQuestionChange(index, 'data_type', newType); handleQuestionChange(index, 'answer_options', []); } }; const handleContentChange = (e) => { const value = e.target.value; if (value.length <= MAX_CONTENT_LENGTH) { handleQuestionChange(index, 'content', value); } }; const handleAnswerChangeForInput = (e) => { const value = e.target.value; if (value.length <= MAX_ANSWER_LENGTH) { handleQuestionChange(index, 'correct_answer', value); } }; const handleAddOption = () => { const trimmedOption = deepTrim(newOption); if (!trimmedOption) { return; } if (trimmedOption.length > MAX_OPTION_LENGTH) { return; } const currentOptions = question.answer_options || []; if (currentOptions.length >= MAX_OPTIONS_COUNT) { return; } if (currentOptions.some(opt => deepTrim(opt) === trimmedOption)) { return; } const updatedOptions = [...currentOptions, trimmedOption]; handleQuestionChange(index, 'answer_options', updatedOptions); setNewOption(''); }; const handleRemoveOption = (optionIndex) => { const currentOptions = question.answer_options || []; const updatedOptions = currentOptions.filter((_, idx) => idx !== optionIndex); const removedOption = currentOptions[optionIndex]; let newCorrectAnswer = question.correct_answer; if (removedOption === question.correct_answer) { newCorrectAnswer = ''; } handleQuestionChange(index, 'answer_options', updatedOptions); if (newCorrectAnswer !== question.correct_answer) { handleQuestionChange(index, 'correct_answer', newCorrectAnswer); } }; const handleOptionTextChange = (optionIndex, value) => { if (value.length <= MAX_OPTION_LENGTH) { const currentOptions = question.answer_options || []; const updatedOptions = [...currentOptions]; const oldOptionText = updatedOptions[optionIndex]; updatedOptions[optionIndex] = value; let newCorrectAnswer = question.correct_answer; if (deepTrim(oldOptionText) === deepTrim(question.correct_answer)) { newCorrectAnswer = value; } handleQuestionChange(index, 'answer_options', updatedOptions); if (newCorrectAnswer !== question.correct_answer) { handleQuestionChange(index, 'correct_answer', newCorrectAnswer); } } }; const handleNewOptionChange = (e) => { const value = e.target.value; if (value.length <= MAX_OPTION_LENGTH) { setNewOption(value); } }; const handleQuestionFileChangeWrapper = (e) => { if (!folder) { return; } handleQuestionFileChange(index, e); }; const renderOptionsSection = () => { if (question.data_type !== 'select') return null; const options = question.answer_options || []; return ( <div className="mt-4"> <div className="flex justify-between items-center mb-2"> <label className="block text-sm font-medium text-gray-700"> Варианты ответов: <span className="text-sm text-gray-500 ml-2"> {options.length}/{MAX_OPTIONS_COUNT} </span> </label> {!question.correct_answer && options.length > 0 && ( <span className="text-sm text-red-500"> Выберите правильный вариант ответа в поле ниже! </span> )} </div> <div className="space-y-2 mb-3 max-h-60 overflow-y-auto"> {options.map((option, optionIndex) => ( <div key={optionIndex} className="flex items-center p-2 border border-gray-200 rounded-md"> <span className="mr-3 text-sm text-gray-600 min-w-6"> {optionIndex + 1}. </span> <input type="text" value={option} onChange={(e) => handleOptionTextChange(optionIndex, e.target.value)} className="flex-grow p-1 border border-gray-300 rounded text-sm" maxLength={MAX_OPTION_LENGTH} placeholder={`Вариант ${optionIndex + 1}`} /> <button type="button" onClick={() => handleRemoveOption(optionIndex)} className="ml-2 text-red-500 hover:text-red-700" > <DeleteIcon className="w-5 h-5" /> </button> </div> ))} </div> {options.length < MAX_OPTIONS_COUNT && ( <div className="flex items-center"> <input type="text" value={newOption} onChange={handleNewOptionChange} className="flex-grow p-2 border border-gray-300 rounded-md text-sm mr-2" maxLength={MAX_OPTION_LENGTH} placeholder="Введите вариант ответа" onKeyPress={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddOption(); } }} /> <button type="button" onClick={handleAddOption} className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 text-sm" > Добавить </button> </div> )} </div> ); }; const renderCorrectAnswerField = () => { if (question.data_type === 'select') { const options = question.answer_options || []; if (options.length === 0) { return ( <div className="text-sm text-gray-500 italic"> Добавьте варианты ответов выше, затем выберите правильный </div> ); } return ( <select value={question.correct_answer || ''} onChange={(e) => handleQuestionChange(index, 'correct_answer', e.target.value)} className="w-full p-2 border border-gray-300 rounded-md" required={question.data_type === 'select'} > <option value="">Выберите правильный ответ из списка</option> {options.map((option, idx) => ( <option key={idx} value={option}> {option} </option> ))} </select> ); } else { return ( <input type="text" value={question.correct_answer || ''} onChange={handleAnswerChangeForInput} className="w-full p-2 border border-gray-300 rounded-md" maxLength={MAX_ANSWER_LENGTH} required={question.data_type === 'input'} placeholder="Правильный ответ" /> ); } }; return ( <div className="border border-gray-200 rounded-md p-4 mb-4"> <div className="flex items-center justify-between mb-3"> <span className="font-medium">Вопрос #{index + 1}</span> <button type="button" onClick={() => handleRemoveQuestion(index)} className="text-red-500 hover:text-red-700" > <DeleteIcon className="w-6 h-6" /> </button> </div> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Тип вопроса </label> <select value={question.data_type || 'input'} onChange={handleDataTypeChange} className="w-full p-2 border border-gray-300 rounded-md" > <option value="input">Ввод ответа</option> <option value="select">Выбор ответа</option> </select> </div> <div className="md:col-span-2"> <label className="block text-sm font-medium text-gray-700 mb-1"> Содержание <span className="text-sm text-gray-500 ml-2"> {question.content?.length || 0}/{MAX_CONTENT_LENGTH} </span> </label> <input type="text" value={question.content || ''} onChange={handleContentChange} className="w-full p-2 border border-gray-300 rounded-md" maxLength={MAX_CONTENT_LENGTH} required disabled={!folder} placeholder={folder ? "Введите текст или путь к изображению" : "Сначала укажите папку для изображений"} /> <input type="file" className={`mt-2 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold ${!folder ? 'file:bg-gray-200 file:text-gray-500 cursor-not-allowed' : 'file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100'}`} onChange={handleQuestionFileChangeWrapper} accept="image/*" disabled={!folder} /> {question.content?.startsWith('/') && !validateImagePath(question.content) && ( <p className="text-red-500 text-sm mt-1"> Папка в пути изображения не совпадает с указанной папкой! </p> )} </div> </div> {renderOptionsSection()} <div className="mt-4"> <label className="block text-sm font-medium text-gray-700 mb-1"> Правильный ответ {question.data_type === 'input' && ( <span className="text-sm text-gray-500 ml-2"> {question.correct_answer?.length || 0}/{MAX_ANSWER_LENGTH} </span> )} </label> {renderCorrectAnswerField()} </div> </div> ); }; export default AddTestQuestion;