add page completing survey

This commit is contained in:
Tatiana Nikolaeva 2025-05-26 03:06:47 +05:00
parent 8182bc1c43
commit 4f1a7cc434
15 changed files with 333 additions and 120 deletions

View file

@ -8,6 +8,8 @@ import {Results} from "./components/Results/Results.tsx";
import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx";
import AuthForm from "./pages/AuthForm/AuthForm.tsx";
import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx";
import CompleteSurvey from "./pages/CompleteSurvey/CompleteSurvey.tsx";
import CompletingSurvey from "./components/CompletingSurvey/CompletingSurvey.tsx";
const App = () => {
return(
@ -31,6 +33,10 @@ const App = () => {
<Route path="results" element={<Results />} />
</Route>
<Route path='/complete-survey' element={<CompleteSurvey/>}>
<Route index element={<CompletingSurvey/>}/>
</Route>
<Route path="*" element={<AuthForm />} />
</Routes>
</BrowserRouter>

View file

@ -3,17 +3,22 @@ import styles from'./AnswerOption.module.css';
import Delete from '../../assets/delete.svg?react';
import Single from '../../assets/radio_button_unchecked.svg?react';
import Multiple from '../../assets/emptyCheckbox.svg?react';
import SelectedSingle from '../../assets/radio_button_checked.svg?react'
import SelectedMultiple from '../../assets/check_box.svg?react';
import TextareaAutosize from 'react-textarea-autosize';
interface AnswerOptionProps{
index: number;
value: string;
onChange: (value: string) => void;
onDelete:(index: number) => void;
onChange?: (value: string) => void;
onDelete?:(index: number) => void;
selectedType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
toggleSelect: () => void;
isSelected?: boolean;
toggleSelect?: () => void;
isCompleteSurveyActive?: boolean;
}
const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDelete, selectedType, toggleSelect}) => {
const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect, isCompleteSurveyActive = false}) => {
const [currentValue, setCurrentValue] = useState(value);
const [isEditing, setIsEditing] = useState(false);
@ -45,7 +50,7 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
const handleSave = () => {
setIsEditing(false);
onChange(currentValue);
onChange?.(currentValue);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
@ -65,16 +70,50 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
}
}, [isEditing]);
const handleMarkerClick = () => {
if (isCompleteSurveyActive && toggleSelect) {
toggleSelect();
}
};
return (
<div className={styles.answer}>
<button
className={`${styles.buttonMarker} ${isEditing ? styles.editing : ''}`}
onClick={toggleSelect}
>
{selectedType === 'SingleAnswerQuestion' ? < Single className={styles.answerIcon} /> : <Multiple className={styles.answerIcon} />}
</button>
{isEditing ? (
<textarea
{isCompleteSurveyActive ? (
<button
className={`${styles.buttonMarker} ${isSelected ? styles.selected : ''}`}
onClick={handleMarkerClick}
>
{selectedType === 'SingleAnswerQuestion' ? (
isSelected ? (
<SelectedSingle className={styles.answerIcon} />
) : (
<Single className={styles.answerIcon} />
)
) : (
isSelected ? (
<SelectedMultiple className={styles.answerIcon} />
) : (
<Multiple className={styles.answerIcon} />
)
)}
</button>
) : (
<button className={styles.buttonMarker}>
{selectedType === 'SingleAnswerQuestion' ? (
<Single className={styles.answerIcon} />
) : (
<Multiple className={styles.answerIcon} />
)}
</button>
)}
{isCompleteSurveyActive ? (
<button className={styles.textAnswer}>
{currentValue || `Ответ ${index}`}
</button>
) : isEditing ? (
<TextareaAutosize
className={styles.answerInput}
ref={textAreaRef}
value={currentValue}
@ -88,12 +127,14 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
{currentValue || `Ответ ${index}`}
</button>
)}
<button className={styles.deleteButton} onClick={() => onDelete(index)}>
<Delete />
</button>
{!isCompleteSurveyActive && (
<button className={styles.deleteButton} onClick={() => onDelete?.(index)}>
<Delete />
</button>
)}
</div>
);
};
export default AnswerOption;

View file

@ -0,0 +1,7 @@
.survey{
width: 68%;
background-color: #F6F6F6;
max-width: 100vw;
min-height: 100vh;
padding: 34px 16%;
}

View file

@ -0,0 +1,32 @@
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx";
import {useState} from "react";
import styles from './CompletingSurvey.module.css'
export const CompletingSurvey = () => {
const [titleSurvey, setTitleSurvey] = useState("Название опроса");
const [descriptionSurvey, setDescriptionSurvey] = useState("");
const [questions, setQuestions] = useState<Question[]>([
{ id: 1, text: 'Вопрос 1', questionType: 'SingleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' },
{ id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]},
{ id: 2, text: 'Вопрос 2', questionType: 'MultipleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' },
{ id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]}
]);
return (
<div className={styles.survey}>
<SurveyInfo
titleSurvey={titleSurvey}
descriptionSurvey={descriptionSurvey}
setDescriptionSurvey={setDescriptionSurvey}
setTitleSurvey={setTitleSurvey}
/>
<QuestionsList
questions={questions}
setQuestions={setQuestions}
/>
</div>
)
}
export default CompletingSurvey

View file

@ -12,8 +12,7 @@
gap: 60px;
list-style: none;
align-items: center;
margin-right: 40%;
margin-right: 20%;
}
.pageLink{

View file

@ -7,9 +7,13 @@ import {Link, useLocation, useNavigate} from "react-router-dom";
const Header: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const isCreateSurveyActive = location.pathname.includes('/survey/create');
const isSurveyPage = location.pathname.includes('/survey/') && !location.pathname.includes('/survey/create');
const isMySurveysPage = location.pathname === '/my-surveys' || isSurveyPage;
const isCreateSurveyActive = location.pathname.startsWith('/survey/create');
const isMySurveysActive = location.pathname === '/my-surveys';
const isCompleteSurveyActive = location.pathname === '/complete-survey';
const isSurveyViewPage = location.pathname.startsWith('/survey/') &&
!location.pathname.startsWith('/survey/create');
const handleLogoClick = () => {
navigate(location.pathname, { replace: true });
@ -19,15 +23,26 @@ const Header: React.FC = () => {
<div className={styles.header}>
<Logo href={location.pathname} onClick={handleLogoClick} />
<nav className={styles.pagesNav}>
<Link to='/survey/create/questions'
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}>
<Link
to='/survey/create/questions'
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}
>
Создать опрос
{isCreateSurveyActive && <hr className={styles.activeLine}/>}
</Link>
<Link to='/my-surveys'
className={`${styles.pageLink} ${isMySurveysPage ? styles.active : ''}`}>
<Link
to='/my-surveys'
className={`${styles.pageLink} ${isMySurveysActive || isSurveyViewPage ? styles.active : ''}`}
>
Мои опросы
{isMySurveysPage && <hr className={styles.activeLine}/>}
{(isMySurveysActive || isSurveyViewPage) && <hr className={styles.activeLine}/>}
</Link>
<Link
to='/complete-survey'
className={`${styles.pageLink} ${isCompleteSurveyActive ? styles.active : ''}`}
>
Прохождение опроса
{isCompleteSurveyActive && <hr className={styles.activeLine}/>}
</Link>
</nav>
<Account href={'/profile'} />

View file

@ -1,9 +1,9 @@
.main {
background-color: #F6F6F6;
width: 100%;
max-width: 100vw;
min-height: 100vh;
padding: 34px 10%;
padding-top: 34px;
padding-left: 12%;
}
.survey {

View file

@ -10,6 +10,8 @@ import {
getAnswerVariants,
updateAnswerVariant
} from "../../api/AnswerApi.ts";
import {useLocation} from "react-router-dom";
import TextareaAutosize from "react-textarea-autosize";
interface QuestionItemProps {
questionId: number;
@ -43,6 +45,10 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
const [questionType, setQuestionType] = useState<'SingleAnswerQuestion' | 'MultipleAnswerQuestion'>(initialQuestionType);
const textareaQuestionRef = useRef<HTMLTextAreaElement>(null);
const location = useLocation();
const isCompleteSurveyActive = location.pathname === '/complete-survey';
useEffect(() => {
setTextQuestion(valueQuestion);
}, [valueQuestion]);
@ -162,62 +168,81 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
};
const toggleSelect = (index: number) => {
if (questionType === 'SingleAnswerQuestion') {
if (initialQuestionType === 'SingleAnswerQuestion') {
// Для одиночного выбора: заменяем массив одним выбранным индексом
setSelectedAnswers([index]);
} else {
setSelectedAnswers((prev) => {
if (prev.includes(index)) {
return prev.filter((i) => i !== index);
} else {
return [...prev, index];
}
});
// Для множественного выбора: добавляем/удаляем индекс
setSelectedAnswers(prev =>
prev.includes(index)
? prev.filter(i => i !== index)
: [...prev, index]
);
}
};
return (
<div className={styles.questionCard}>
<div className={styles.questionContainer}>
<div className={styles.question}>
{isEditingQuestion ? (
<textarea
className={styles.questionTextarea}
ref={textareaQuestionRef}
value={textQuestion === initialTextQuestion ? '' : textQuestion}
onChange={handleTextareaQuestionChange}
onKeyDown={handleQuestionKeyDown}
onBlur={handleQuestionBlur}
placeholder={initialTextQuestion}
rows={1}
{isCompleteSurveyActive ? (
<div>
<div className={styles.questionContainer}>
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
</div>
{initialAnswerVariants.map((answer, index) => (
<AnswerOption
key={answer.id || index}
selectedType={initialQuestionType}
index={index + 1}
value={answer.text}
isSelected={selectedAnswers.includes(index)}
toggleSelect={() => toggleSelect(index)}
isCompleteSurveyActive={isCompleteSurveyActive}
/>
) : (
<button className={styles.buttonQuestion} onClick={handleQuestionClick}>
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
</button>
)}
<TypeDropdown selectedType={questionType} onTypeChange={handleTypeChange}/>
))}
</div>
) : (
<div className={styles.questionContainer}>
<div className={styles.question}>
{isEditingQuestion ? (
<TextareaAutosize
className={styles.questionTextarea}
ref={textareaQuestionRef}
value={textQuestion === initialTextQuestion ? '' : textQuestion}
onChange={handleTextareaQuestionChange}
onKeyDown={handleQuestionKeyDown}
onBlur={handleQuestionBlur}
placeholder={initialTextQuestion}
rows={1}
/>
) : (
<button className={styles.buttonQuestion} onClick={handleQuestionClick}>
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
</button>
)}
<TypeDropdown selectedType={questionType} onTypeChange={handleTypeChange}/>
</div>
{initialAnswerVariants.map((answer, index) => (
<AnswerOption
key={answer.id || index}
selectedType={questionType}
index={index + 1}
value={answer.text}
onChange={(value) => handleAnswerChange(index, value)}
onDelete={() => handleDeleteAnswer(index)}
toggleSelect={() => toggleSelect(index)}
/>
))}
{initialAnswerVariants.map((answer, index) => (
<AnswerOption
key={answer.id || index}
selectedType={questionType}
index={index + 1}
value={answer.text}
onChange={(value) => handleAnswerChange(index, value)}
onDelete={() => handleDeleteAnswer(index)}
toggleSelect={() => toggleSelect(index)}
/>
))}
<div className={styles.questionActions}>
<AddAnswerButton onClick={handleAddAnswer} />
<button className={styles.deleteQuestionButton} onClick={handleDeleteQuestion}>
Удалить
<Delete className={styles.basketImg}/>
</button>
</div>
</div>
<div className={styles.questionActions}>
<AddAnswerButton onClick={handleAddAnswer} />
<button className={styles.deleteQuestionButton} onClick={handleDeleteQuestion}>
Удалить
<Delete className={styles.basketImg}/>
</button>
</div>
</div>)
}
</div>
);
};

View file

@ -1,2 +1,16 @@
/*QuestionsList.module.css*/
.departur_button{
display: block;
margin: 10px auto;
padding: 25px 50.5px;
border: none;
border-radius: 20px;
background-color: #3788D6;
color: white;
font-weight: 700;
font-size: 24px;
text-align: center;
box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1);
box-sizing: border-box;
}

View file

@ -3,6 +3,8 @@ import QuestionItem from "../QuestionItem/QuestionItem.tsx";
import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx";
import {addNewQuestion, deleteQuestion, getListQuestions} from "../../api/QuestionApi.ts";
import {addNewAnswerVariant} from "../../api/AnswerApi.ts";
import {useLocation} from "react-router-dom";
import styles from './QuestionsList.module.css'
interface QuestionsListProps {
questions: Question[];
@ -21,6 +23,9 @@ export interface Question {
}
const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, surveyId}) => {
const location = useLocation();
const isCompleteSurveyActive = location.pathname === '/complete-survey';
const handleAddQuestion = async () => {
if (!surveyId) {
const newQuestion: Question = {
@ -123,7 +128,10 @@ const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, s
surveyId={surveyId}
/>
))}
<AddQuestionButton onClick={handleAddQuestion} />
{!isCompleteSurveyActive ? <AddQuestionButton onClick={handleAddQuestion} /> : (
<button className={styles.departur_button}>Отправить</button>
)}
</>
);
};

View file

@ -7,37 +7,45 @@
margin-top: 34px;
margin-bottom: 49px;
border-radius: 14px;
min-height: 191px;
/*min-height: 191px;*/
/*max-height: 100vh;*/
max-height: fit-content;
}
.info{
min-width: 373px;
display: block;
/*display: block;*/
padding: 35px;
display: flex; /* Добавляем flex */
flex-direction: column; /* Элементы в колонку */
align-items: center;
}
.titleSurvey{
width: 80%;
display: block;
border: none;
margin: 0 auto;
margin: 0 auto 13px;
background-color: white;
text-align: center;
font-size: 20px;
font-weight: 600;
margin-bottom: 23px;
/*margin-bottom: 23px;*/
/*margin-bottom: 15px;*/
word-break: break-word;
padding: 0;
}
.textareaTitle,
.textareaDescrip {
width: 100%;
width: 80%;
max-width: 100%;
resize: none;
border: none;
outline: none;
font-family: inherit;
padding: 0;
margin: 0;
margin: 0 auto;
background: transparent;
display: block;
overflow-y: hidden;
@ -48,7 +56,7 @@
font-weight: 600;
text-align: center;
line-height: 1.2;
min-height: 40px;
min-height: 60px;
}
.textareaDescrip {
@ -67,7 +75,7 @@
.description {
border: none;
font-size: 18px;
font-size: 24px;
font-weight: 500;
text-align: center;
background-color: white;
@ -78,6 +86,15 @@
word-break: break-word;
}
.desc{
font-size: 24px;
font-weight: 500;
background-color: white;
max-width: 80%;
word-break: break-word;
margin: 0;
text-align: center;
}
.descripButton{
border: none;
@ -98,3 +115,10 @@
color: #7D7983;
padding: 10px;
}
.createdAt{
text-align: center;
font-size: 18px;
font-weight: 500;
color: #7D7983;
}

View file

@ -2,27 +2,34 @@ import React, {useState, useRef, useEffect} from "react";
import styles from './SurveyInfo.module.css'
import AddDescripImg from '../../assets/add_circle.svg?react';
import TextareaAutosize from 'react-textarea-autosize';
import {useLocation} from "react-router-dom";
interface SurveyInfoProps {
titleSurvey: string;
descriptionSurvey: string;
setDescriptionSurvey: (text: string) => void;
setTitleSurvey: (text: string) => void;
setDescriptionSurvey?: (text: string) => void;
setTitleSurvey?: (text: string) => void;
createdAt?: Date;
}
const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurvey, descriptionSurvey, setTitleSurvey}) => {
const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurvey, descriptionSurvey, setTitleSurvey, createdAt = new Date()}) => {
const [showDescriptionField, setShowDescriptionField] = useState(false);
const [showNewTitleField, setShowNewTitleField] = useState(false);
const titleTextareaRef = useRef<HTMLTextAreaElement>(null);
const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
const location = useLocation();
const isCompleteSurveyActive = location.pathname === '/complete-survey';
const isSurveyViewPage = location.pathname.startsWith('/survey/') &&
!location.pathname.startsWith('/survey/create');
const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescriptionSurvey(descripEvent.target.value);
setDescriptionSurvey?.(descripEvent.target.value);
};
const handleNewTitleChange = (titleEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitleSurvey(titleEvent.target.value);
setTitleSurvey?.(titleEvent.target.value);
};
useEffect(() => {
@ -72,8 +79,52 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
setShowDescriptionField(false);
};
const addDate = () => {
const year = createdAt.getFullYear();
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
const day = String(createdAt.getDate()).padStart(2, '0');
return `${day}/${month}/${year}`;
}
const renderTitle = () => {
if (isSurveyViewPage || isCompleteSurveyActive) {
return (
<button className={styles.titleSurvey}>
<h1>{titleSurvey || 'Название опроса'}</h1>
</button>
)
}
if (showNewTitleField) {
return (
<h1 className={styles.titleSurvey}>
<TextareaAutosize
className={styles.textareaTitle}
ref={titleTextareaRef}
value={titleSurvey === 'Название опроса' ? '' : titleSurvey}
placeholder={'Название опроса'}
onChange={handleNewTitleChange}
onKeyDown={handleTitleKeyDown}
onBlur={handleTitleBlur}
/>
</h1>
);
}
return (
<button className={styles.titleSurvey} onClick={handleAddNewTitleClick}>
<h1>{titleSurvey || 'Название опроса'}</h1>
</button>
);
};
const renderDescription = () => {
if (isSurveyViewPage || isCompleteSurveyActive) {
return descriptionSurvey ? (
<p className={styles.desc}>{descriptionSurvey}</p>
) : 'Описание';
}
if (descriptionSurvey && !showDescriptionField) {
return (
<button className={styles.description} onClick={handleParagraphClick}>
@ -106,35 +157,21 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
</button>
);
}
}
};
return (
<div className={styles.blockInfo}>
<div className={styles.info}>
{
showNewTitleField ? (
<h1 className={styles.titleSurvey}>
<TextareaAutosize className={styles.textareaTitle}
ref={titleTextareaRef}
value={titleSurvey === 'Название опроса' ? '' : titleSurvey}
placeholder={'Название опроса'}
onChange={handleNewTitleChange}
onKeyDown={handleTitleKeyDown}
onBlur={handleTitleBlur}
/>
</h1>
) : (
<button className={styles.titleSurvey} onClick={handleAddNewTitleClick}>
<h1>{titleSurvey || 'Название опроса'}</h1>
</button>
)
}
{renderTitle()}
{renderDescription()}
{(isSurveyViewPage || isCompleteSurveyActive) && createdAt && (
<p className={styles.createdAt}>Дата создания: {addDate()}</p>
)}
</div>
</div>
);
};
export default SurveyInfo;

View file

@ -0,0 +1,3 @@
.layout{
width: 100%;
}

View file

@ -0,0 +1,14 @@
import Header from "../../components/Header/Header.tsx";
import styles from './CompleteSurvey.module.css'
import CompletingSurvey from "../../components/CompletingSurvey/CompletingSurvey.tsx";
export const CompleteSurvey = () => {
return(
<div className={styles.layout}>
<Header/>
<CompletingSurvey/>
</div>
)
}
export default CompleteSurvey

View file

@ -3,15 +3,3 @@
.layout{
width: 100%;
}
.main{
width: 100%;
min-height: 85vh;
display: flex;
background-color: #F6F6F6;
}
.content{
width: 100%;
margin-left: 8.9%;
}