/
TimurIsm
/
OpenEyes
Обзор
Документация
Войти
/
TimurIsm
/
OpenEyes
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
front/src/components/QuestionModalBase.jsx
364 строки
15 KB
TimurIsm
first_commit
16 май 2026, 15:34
16 май 2026, 15:34
4d3f198
Код
Авторство
О чём код?
import React, { useState, useEffect } from 'react'; import { AddButton, SaveButton, CancelButton } from '../buttons/ModalButton'; import { DeleteIcon } from '../buttons/IconButton'; import AlertMessage from '../components/AlertMessage'; import { fetchQuestions } from '../http/surveyAPI'; import { deepTrim } from '../components/stringUtils'; import '../index.css'; const MAX_QUESTION_LENGTH = 250; const MAX_ANSWER_LENGTH = 100; const MAX_ANSWERS_COUNT = 10; const REQUIRED_CUSTOM_ANSWER = "Другое (укажите свой вариант)"; const QuestionModalBase = ({ isOpen, onClose, onSave, title, initialQuestion, isEditMode, questionType: propQuestionType }) => { const [formState, setFormState] = useState({ questionText: '', questionType: 'single', answers: [], newAnswer: '', }); const [alertMessage, setAlertMessage] = useState(null); const [originalQuestion, setOriginalQuestion] = useState(null); useEffect(() => { if (isOpen) { setOriginalQuestion(initialQuestion); const answers = initialQuestion?.answers || []; setFormState({ questionText: initialQuestion?.text || '', questionType: isEditMode ? propQuestionType : 'single', answers: isEditMode ? [...answers] : [], newAnswer: '', }); document.body.style.overflow = 'hidden'; } else { setAlertMessage(null); document.body.style.overflow = 'auto'; } return () => { document.body.style.overflow = 'auto'; }; }, [isOpen, initialQuestion, isEditMode, propQuestionType]); const checkQuestionExists = async (questionText) => { try { const questions = await fetchQuestions(); return questions.some(q => isEditMode ? q.id !== initialQuestion.id && q.text.toLowerCase() === questionText.toLowerCase() : q.text.toLowerCase() === questionText.toLowerCase()); } catch (error) { console.error('Ошибка при проверке вопроса:', error); return false; } }; const handleAddAnswer = () => { if (formState.answers.length >= MAX_ANSWERS_COUNT) { setAlertMessage({ type: 'error', message: `Максимальное количество ответов - ${MAX_ANSWERS_COUNT}!`}); return; } const trimmedAnswer = deepTrim(formState.newAnswer); if (!trimmedAnswer) { setAlertMessage({ type: 'error', message: 'Текст ответа не может быть пустым!' }); return; } if (trimmedAnswer.length > MAX_ANSWER_LENGTH) { setAlertMessage({ type: 'error', message: `Текст ответа не может превышать ${MAX_ANSWER_LENGTH} символов!`}); return; } const answerExists = formState.answers.some(answer => { const existingAnswer = isEditMode ? answer.text : answer; return existingAnswer.toLowerCase() === trimmedAnswer.toLowerCase(); }); if (answerExists && trimmedAnswer !== REQUIRED_CUSTOM_ANSWER) { setAlertMessage({ type: 'error', message: 'Такой вариант ответа уже существует!' }); return; } if (formState.questionType === 'custom' && trimmedAnswer === REQUIRED_CUSTOM_ANSWER) { const hasRequiredAnswer = formState.answers.some(answer => isEditMode ? answer.text === REQUIRED_CUSTOM_ANSWER : answer === REQUIRED_CUSTOM_ANSWER); if (hasRequiredAnswer) { setAlertMessage({ type: 'error', message: `Вариант "${REQUIRED_CUSTOM_ANSWER}" уже добавлен!`}); return; } } if (formState.questionType === 'custom' && formState.answers.some(answer => isEditMode ? answer.text === REQUIRED_CUSTOM_ANSWER : answer === REQUIRED_CUSTOM_ANSWER)) { const indexOfRequired = formState.answers.findIndex(answer => isEditMode ? answer.text === REQUIRED_CUSTOM_ANSWER : answer === REQUIRED_CUSTOM_ANSWER); const newAnswers = [...formState.answers]; const newAnswerObj = isEditMode ? { id: null, text: trimmedAnswer } : trimmedAnswer; newAnswers.splice(indexOfRequired, 0, newAnswerObj); setFormState(prevState => ({ ...prevState, answers: newAnswers, newAnswer: '', })); } else { const newAnswer = isEditMode ? { id: null, text: trimmedAnswer } : trimmedAnswer; setFormState(prevState => ({ ...prevState, answers: [...prevState.answers, newAnswer], newAnswer: '', })); } setAlertMessage(null); }; const handleDeleteAnswer = (index) => { if (formState.questionType === 'custom') { const answerText = isEditMode ? formState.answers[index].text : formState.answers[index]; if (answerText === REQUIRED_CUSTOM_ANSWER) { setAlertMessage({ type: 'error', message: `Вариант "${REQUIRED_CUSTOM_ANSWER}" нельзя удалить!`}); return; } } setFormState(prevState => ({ ...prevState, answers: prevState.answers.filter((_, i) => i !== index), })); }; const handleSave = async () => { const trimmedQuestion = deepTrim(formState.questionText); if (!trimmedQuestion) { setAlertMessage({ type: 'error', message: 'Текст вопроса не может быть пустым!' }); return; } if (trimmedQuestion.length > MAX_QUESTION_LENGTH) { setAlertMessage({ type: 'error', message: `Текст вопроса не может превышать ${MAX_QUESTION_LENGTH} символов!`}); return; } if (formState.answers.length === 0) { setAlertMessage({ type: 'error', message: 'Добавьте хотя бы один ответ!' }); return; } const cleanedAnswers = formState.answers.map(answer => { const answerText = isEditMode ? deepTrim(answer.text) : deepTrim(answer); return { ...answer, text: answerText }; }); const hasEmptyAnswer = cleanedAnswers.some(answer => !answer.text); if (hasEmptyAnswer) { setAlertMessage({ type: 'error', message: 'Все ответы должны быть заполнены!' }); return; } if (formState.questionType === 'custom') { const requiredAnswers = cleanedAnswers.filter(answer => answer.text === REQUIRED_CUSTOM_ANSWER); if (requiredAnswers.length !== 1) { setAlertMessage({ type: 'error', message: `Для вопросов этого типа требуется ровно один вариант "${REQUIRED_CUSTOM_ANSWER}" в конце списка!`}); return; } const lastAnswer = cleanedAnswers[cleanedAnswers.length - 1].text; if (lastAnswer !== REQUIRED_CUSTOM_ANSWER) { setAlertMessage({ type: 'error', message: `Вариант "${REQUIRED_CUSTOM_ANSWER}" должен быть последним в списке ответов!`}); return; } } try { const questionExists = await checkQuestionExists(trimmedQuestion); if (questionExists) { setAlertMessage({ type: 'error', message: 'Вопрос с таким текстом уже существует!'}); return; } const answers = isEditMode ? cleanedAnswers.map(answer => ({ id: answer.id || null, text: answer.text })) : cleanedAnswers.map(answer => ({ text: answer.text })); onSave({ ...(isEditMode ? initialQuestion : {}), text: trimmedQuestion, type: formState.questionType, answers, }); if (!isEditMode) { setFormState({ questionText: '', questionType: 'single', answers: [], newAnswer: '', }); } setAlertMessage(null); onClose(); } catch (error) { setAlertMessage({ type: 'error', message: 'Ошибка при сохранении вопроса! Пожалуйста, попробуйте снова.'}); } }; const handleCancel = () => { if (isEditMode && originalQuestion) { setFormState({ questionText: originalQuestion.text, answers: [...originalQuestion.answers], newAnswer: '', questionType: propQuestionType, }); } else { setFormState({ questionText: '', questionType: 'single', answers: [], newAnswer: '', }); } setAlertMessage(null); onClose(); }; const handleQuestionTextChange = (e) => { const value = e.target.value; if (value.length <= MAX_QUESTION_LENGTH) { setFormState(prevState => ({ ...prevState, questionText: value })); } }; const handleAnswerTextChange = (e, index) => { if (isEditMode) { const value = e.target.value; if (value.length <= MAX_ANSWER_LENGTH) { const updatedAnswers = [...formState.answers]; updatedAnswers[index].text = value; setFormState(prevState => ({ ...prevState, answers: updatedAnswers })); } } }; const handleNewAnswerChange = (e) => { const value = e.target.value; if (value.length <= MAX_ANSWER_LENGTH) { setFormState(prevState => ({ ...prevState, newAnswer: value })); } }; const handleQuestionTypeChange = (e) => { if (!isEditMode) { setFormState(prevState => ({ ...prevState, questionType: e.target.value })); } }; if (!isOpen) return null; return ( <div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center"> <div className="bg-white p-6 rounded-lg w-11/12 lg:w-1/2 max-h-[90vh] min-h-[50vh] flex flex-col"> <h2 className="text-xl font-bold mb-4 mt-4 text-center">{title}</h2> <span className="separator" /> <div className="mb-4"> <label htmlFor="questionText" className="block mb-2 text-md font-medium text-gray-900"> Текст вопроса <span className="text-sm text-gray-500 ml-2"> {formState.questionText.length}/{MAX_QUESTION_LENGTH} </span> </label> <input id="questionText" type="text" placeholder="Текст вопроса" value={formState.questionText} onChange={handleQuestionTextChange} className="w-full p-2 border border-gray-400 rounded-lg" maxLength={MAX_QUESTION_LENGTH} /> </div> <div className="mb-4"> {isEditMode ? ( <> <span className="font-semibold text-md">Тип вопроса:</span> <span className="ml-2 text-md">{propQuestionType}</span> </> ) : ( <> <span className="text-gray-900 mr-4 text-md">Тип вопроса:</span> <select value={formState.questionType} onChange={handleQuestionTypeChange} className="w-1/2 p-2 border border-gray-400 rounded-lg" > <option value="single">Один ответ</option> <option value="multiple">Несколько ответов</option> <option value="custom">Собственный ответ</option> </select> </> )} </div> <div className="mb-4 flex-grow overflow-y-auto"> <h3 className="text-md font-semibold mb-2">Ответы:</h3> <span className="text-sm text-gray-500"> {formState.answers.length}/{MAX_ANSWERS_COUNT} </span> {formState.answers.map((answer, index) => ( <div key={index} className="flex items-center mb-2"> {isEditMode ? ( <input type="text" value={answer.text} onChange={(e) => handleAnswerTextChange(e, index)} className="w-full p-2 border border-gray-400 rounded-lg mr-2" maxLength={MAX_ANSWER_LENGTH} /> ) : ( <span className="mr-2 flex-grow p-2 border border-gray-200 rounded bg-gray-50"> {answer} </span> )} <button onClick={() => handleDeleteAnswer(index)} className="text-gray-500 hover:text-gray-700 ml-2" > <DeleteIcon className="w-6 h-6" /> </button> </div> ))} <div className="flex items-center mt-4"> <input type="text" placeholder={isEditMode ? "Добавить ответ" : "Текст ответа"} value={formState.newAnswer} onChange={handleNewAnswerChange} className="w-full p-2 border border-gray-400 rounded-lg mr-2" maxLength={MAX_ANSWER_LENGTH} /> <AddButton onClick={handleAddAnswer} /> </div> </div> {alertMessage && ( <div className="mb-4"> <AlertMessage type={alertMessage.type} message={alertMessage.message} /> </div> )} <div className="flex justify-center space-x-4 mt-auto"> <SaveButton onClick={handleSave} /> <CancelButton onClick={handleCancel} /> </div> </div> </div> ); }; export default QuestionModalBase;