api requests

This commit is contained in:
Tatiana Nikolaeva 2025-05-22 20:23:09 +05:00
parent fe74490440
commit 5a1cc7c43c
22 changed files with 665 additions and 133 deletions

View file

@ -1,18 +1,47 @@
import React from 'react';
import styles from './Account.module.css'
import React, { useEffect, useState } from 'react';
import styles from './Account.module.css';
import AccountImg from '../../assets/account.svg?react';
import { getCurrentUser } from '../../api/AuthApi';
interface AccountProps {
href: string;
user: string;
}
const Account: React.FC<AccountProps> = ({href, user}) => {
const Account: React.FC<AccountProps> = ({ href }) => {
const [userName, setUserName] = useState<string>();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchUserData = async () => {
try {
const userData = localStorage.getItem("user");
if (userData) {
const parsedData = JSON.parse(userData);
setUserName(`${parsedData.firstName} ${parsedData.lastName}`);
} else {
const data = await getCurrentUser();
setUserName(`${data.firstName} ${data.lastName}`);
}
} catch (error) {
console.error("Ошибка загрузки данных пользователя:", error);
} finally {
setIsLoading(false);
}
};
fetchUserData();
}, []);
if (isLoading) {
return <div className={styles.account}>Загрузка...</div>;
}
return (
<div className={styles.account}>
<a className={styles.accountText} href={href}>
<AccountImg className={styles.accountImg}/>
{user}
{userName}
</a>
</div>
);

View file

@ -1,11 +1,9 @@
import React from "react";
import Logo from "../Logo/Logo.tsx";
import Account from "../Account/Account.tsx";
import styles from './Header.module.css'
import styles from './Header.module.css';
import {Link, useLocation, useNavigate} from "react-router-dom";
const Header: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
@ -22,19 +20,19 @@ const Header: React.FC = () => {
<Logo href={location.pathname} onClick={handleLogoClick} />
<nav className={styles.pagesNav}>
<Link to='/survey/create/questions'
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}>
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}>
Создать опрос
{isCreateSurveyActive && <hr className={styles.activeLine}/>}
</Link>
<Link to='/my-surveys'
className={`${styles.pageLink} ${isMySurveysPage ? styles.active : ''}`}>
className={`${styles.pageLink} ${isMySurveysPage ? styles.active : ''}`}>
Мои опросы
{isMySurveysPage && <hr className={styles.activeLine}/>}
</Link>
</nav>
<Account href={'/profile'} user={'Иванов Иван'}/>
<Account href={'/profile'} />
</div>
);
};
export default Header;
export default Header;

View file

@ -1,7 +1,8 @@
import styles from './MySurveysList.module.css'
import {useNavigate} from "react-router-dom";
import {useEffect, useState} from "react";
import {getMySurveys, ISurvey} from "../../api/SurveyApi.ts";
import {deleteSurvey, getMySurveys, ISurvey} from "../../api/SurveyApi.ts";
import Delete from '../../assets/delete.svg?react'
interface MySurveyItem extends ISurvey{
@ -35,34 +36,40 @@ export const MySurveyList = () => {
fetchSurvey();
}, [navigate]);
// const surveys: MySurveyItem[] = [
// {
// id: '1',
// title: 'Опрос 1',
// description: 'Описание опроса 1',
// createdBy: '27-04-2025',
// status: 'active',
// },
// {
// id: '2',
// title: 'Опрос 2',
// description: 'Описание опроса 2',
// createdBy: '01-01-2025',
// status: 'completed',
// }
// ]
const handleSurveyClick = (id: number) => {
navigate(`/survey/${id}/questions`)
const handleSurveyClick = (surveyId: number) => {
navigate(`/survey/${surveyId}/questions`)
}
const handleDeleteClick = async (id: number, e: React.MouseEvent) => {
e.stopPropagation();
try {
const response = await deleteSurvey(id);
if (response?.success) {
setSurveys(prev => prev.filter(survey => survey.id !== id));
} else {
console.error('Не удалось удалить опрос')
}
} catch (error) {
console.error('Ошибка при удалении опроса:', error);
if (error instanceof Error && error.message.includes("401")) {
navigate('/login');
} else {
alert("Ошибка при удалении опроса: " + (error instanceof Error ? error.message : 'Неизвестная ошибка'));
}
}
};
return(
<div className={styles.main}>
{surveys.map((survey) => (
<button
<div
key={survey.id}
className={styles.survey}
onClick={() => handleSurveyClick(survey.id)}
role='button'
tabIndex={0}
>
<div className={styles.textContent}>
<div className={styles.surveyData}>
@ -71,12 +78,21 @@ export const MySurveyList = () => {
</div>
<span className={styles.date}>Дата создания: {survey.createdBy}</span>
</div>
<div className={`${styles.status} ${
survey.status === 'active' ? styles.active : styles.completed
}`}>
{survey.status === 'active' ? 'Активен' : 'Завершён'}
<div className={styles.container}>
<div className={`${styles.status} ${
survey.status === 'active' ? styles.active : styles.completed
}`}>
{survey.status === 'active' ? 'Активен' : 'Завершён'}
</div>
<button
onClick={(e) => handleDeleteClick(survey.id, e)}
className={styles.buttonDelete}
>
Удалить опрос {' '}
<Delete className={styles.imgDelete}/>
</button>
</div>
</button>
</div>
))}
</div>
)

View file

@ -1,6 +1,7 @@
.main {
background-color: #F6F6F6;
width: 100%;
max-width: 100vw;
min-height: 100vh;
padding: 34px 10%;
}
@ -26,6 +27,32 @@
flex-direction: column;
}
.container{
display: flex;
flex-direction: column;
justify-content: space-between;
}
.buttonDelete{
border-radius: 8px;
align-items: center;
background-color: #FFFFFF;
border: none;
outline: none;
padding: 5px 3px;
color: black;
font-weight: 500;
font-size: 18px;
}
.buttonDelete:hover{
background-color: #EDEDED;
}
.imgDelete{
vertical-align: middle;
}
.status {
width: fit-content;
height: fit-content;

View file

@ -2,7 +2,6 @@ import React from 'react'
import {useLocation, useNavigate} from 'react-router-dom'
import styles from './Navigation.module.css'
import NavigationItem from "../NavigationItem/NavigationItem.tsx";
import SaveButton from "../SaveButton/SaveButton.tsx";
const Navigation: React.FC = () => {
const location = useLocation();
@ -41,7 +40,6 @@ const Navigation: React.FC = () => {
))}
</ul>
</nav>
<SaveButton />
</div>
);
};

View file

@ -7,16 +7,18 @@ import Delete from '../../assets/deleteQuestion.svg?react';
interface QuestionItemProps {
indexQuestion: number;
questionId: number;
initialTextQuestion?: string;
valueQuestion: string;
onChangeQuestion: (valueQuestion: string) => void;
onDeleteQuestion: (index: number) => void;
onDeleteQuestion: (index: number) => Promise<void>;
selectedType: 'single' | 'multiply'; // Уточняем тип
setSelectedType: (type: 'single' | 'multiply') => void; // Уточняем тип
}
const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQuestion = `Вопрос ${indexQuestion}`,
valueQuestion, onChangeQuestion, onDeleteQuestion}) => {
const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single');
const QuestionItem: React.FC<QuestionItemProps> = ({questionId, initialTextQuestion = `Вопрос ${questionId}`,
valueQuestion, onChangeQuestion, onDeleteQuestion, setSelectedType, selectedType}) => {
// const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single');
const [answerOption, setAnswerOption] = useState(['']);
const [textQuestion, setTextQuestion] = useState(initialTextQuestion);
const [isEditingQuestion, setIsEditingQuestion] = useState(false);
@ -86,8 +88,12 @@ const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQu
setTextQuestion(valueQuestion);
}, [valueQuestion]);
const handleDeleteQuestion = () => {
onDeleteQuestion(indexQuestion);
const handleDeleteQuestion = async () => {
try {
await onDeleteQuestion(questionId);
} catch (error) {
console.error('Ошибка при удалении вопроса:', error);
}
};
const toggleSelect = (index: number) => {

View file

@ -1,28 +1,40 @@
import React, { useState } from "react";
import React, {useEffect, useState} from "react";
import QuestionItem from "../QuestionItem/QuestionItem.tsx";
import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx";
import {deleteQuestion} from "../../api/QuestionApi.ts";
interface QuestionsListProps {}
interface Question {
id: number;
text: string;
interface QuestionsListProps {
questions: Question[];
setQuestions: (questions: Question[]) => void;
surveyId?: number;
}
const QuestionsList: React.FC<QuestionsListProps> = () => {
const [questions, setQuestions] = useState<Question[]>([
{ id: 1, text: '' },
]);
export interface Question {
id: number;
text: string;
questionType: 'singleanswerquestion' | 'multipleanswerquestion';
}
const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, surveyId}) => {
const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single');
const [localQuestionId, setLocalQuestionId] = useState(2); // Начинаем с 2, так как первый вопрос имеет ID=1
const handleAddQuestion = () => {
const maxId = questions.reduce((max, question) => Math.max(max, question.id), 0);
const newQuestion: Question = {
id: maxId + 1,
text: ''
id: localQuestionId,
text: '',
questionType: selectedType === 'single' ? 'singleanswerquestion' : 'multipleanswerquestion',
};
setQuestions([...questions, newQuestion]);
setLocalQuestionId(localQuestionId + 1);
};
useEffect(() => {
setLocalQuestionId(questions.length > 0 ?
Math.max(...questions.map(q => q.id)) + 1 : 1);
}, [questions]);
const handleQuestionChange = (id: number, value: string) => {
const newQuestions = questions.map((question) =>
question.id === id ? { ...question, text: value } : question
@ -30,9 +42,25 @@ const QuestionsList: React.FC<QuestionsListProps> = () => {
setQuestions(newQuestions);
};
const handleDeleteQuestion = (id: number) => {
const newQuestions = questions.filter((question) => question.id !== id);
setQuestions(newQuestions);
const handleDeleteQuestion = async (id: number) => {
try {
if (surveyId) {
const response = await deleteQuestion(surveyId, id);
if (!response?.success) {
throw new Error('Не удалось удалить вопрос на сервере');
}
}
const newQuestions: Question[] = [];
for (const question of questions) {
if (question.id !== id) {
newQuestions.push(question);
}
}
setQuestions(newQuestions);
} catch (error) {
console.error('Ошибка при удалении вопроса:', error);
alert('Не удалось удалить вопрос: ' + (error instanceof Error ? error.message : 'Неизвестная ошибка'));
}
};
return (
@ -40,10 +68,12 @@ const QuestionsList: React.FC<QuestionsListProps> = () => {
{questions.map((question) => (
<QuestionItem
key={question.id}
indexQuestion={question.id}
questionId={question.id}
valueQuestion={question.text}
onDeleteQuestion={() => handleDeleteQuestion(question.id)}
onChangeQuestion={(value) => handleQuestionChange(question.id, value)}
selectedType={selectedType}
setSelectedType={setSelectedType}
/>
))}
<AddQuestionButton onClick={handleAddQuestion} />

View file

@ -1,10 +1,19 @@
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import styles from './Results.module.css'
import {useState} from "react";
export const Results = () => {
const [descriptionSurvey, setDescriptionSurvey] = useState('');
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
return(
<div className={styles.results}>
<SurveyInfo />
<SurveyInfo
titleSurvey={titleSurvey}
descriptionSurvey={descriptionSurvey}
setDescriptionSurvey={setDescriptionSurvey}
setTitleSurvey={setTitleSurvey}
/>
</div>
)
}

View file

@ -1,7 +1,8 @@
/*SaveButton.module.css*/
.createSurveyButton {
margin-left: 40px;
display: block;
margin: 10px auto;
padding: 25px 50.5px;
border: none;
border-radius: 20px;
@ -12,4 +13,4 @@
text-align: center;
box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1);
box-sizing: border-box;
}
}

View file

@ -2,12 +2,12 @@ import React from 'react'
import styles from './SaveButton.module.css'
interface CreateSurveyButtonProps {
// onClick(): void;
onClick(): void;
}
const SaveButton: React.FC<CreateSurveyButtonProps> = () => {
const SaveButton: React.FC<CreateSurveyButtonProps> = ({onClick}) => {
return (
<button className={styles.createSurveyButton}>
<button onClick={onClick} className={styles.createSurveyButton}>
Сохранить
</button>
);

View file

@ -1,13 +1,21 @@
import React from 'react';
import React, {useState} from 'react';
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import styles from "./SettingSurvey.module.css";
import TimeEvent from "../TimeEvent/TimeEvent.tsx";
const SettingSurvey: React.FC = () => {
const [descriptionSurvey, setDescriptionSurvey] = useState('');
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
return (
<div className={styles.settingSurvey}>
<SurveyInfo />
<SurveyInfo
titleSurvey={titleSurvey}
descriptionSurvey={descriptionSurvey}
setDescriptionSurvey={setDescriptionSurvey}
setTitleSurvey={setTitleSurvey}
/>
<div className={styles.startEndTime}>
<TimeEvent title='Время начала'/>
<TimeEvent title='Время окончания'/>

View file

@ -1,13 +1,73 @@
import React from "react";
import React, {useState} from "react";
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import QuestionsList from "../QuestionsList/QuestionsList.tsx";
import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx";
import styles from './Survey.module.css'
import SaveButton from "../SaveButton/SaveButton.tsx";
import {ISurvey, postNewSurvey} from "../../api/SurveyApi.ts";
import {addNewQuestion} from "../../api/QuestionApi.ts";
import {useNavigate} from "react-router-dom";
const Survey: React.FC = () => {
const navigate = useNavigate();
const [descriptionSurvey, setDescriptionSurvey] = useState('');
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
const [survey, setSurvey] = useState<ISurvey | null>(null);
const [questions, setQuestions] = useState<Question[]>([
{ id: 1, text: '', questionType: 'singleanswerquestion'},
]);
// const handleSave = async () => {
// const savedSurvey = await postNewSurvey({title: titleSurvey, description: descriptionSurvey});
// setSurvey(savedSurvey);
// Promise.all(
// questions
// .map((question) => addNewQuestion( savedSurvey.id, {title: question.text, questionType: question.questionType })),
// )
// .then(() => {
// alert('Все удачно сохранилось');
// })
// .catch(() => {
// alert('Пиздец');
// });
// };
const handleSave = async () => {
const savedSurvey = await postNewSurvey({title: titleSurvey, description: descriptionSurvey});
setSurvey(savedSurvey);
try {
await Promise.all(
questions.map(question =>
addNewQuestion(savedSurvey.id, {title: question.text, questionType: question.questionType})
))
// Сбрасываем состояние после сохранения
setQuestions([{ id: 1, text: '', questionType: 'singleanswerquestion' }]);
setTitleSurvey('Новый опрос');
setDescriptionSurvey('');
navigate('/my-surveys');
} catch (error) {
console.error('Ошибка при сохранении:', error);
}
};
return (
<div className={styles.survey}>
<SurveyInfo />
<QuestionsList />
<SurveyInfo
titleSurvey={titleSurvey}
descriptionSurvey={descriptionSurvey}
setDescriptionSurvey={setDescriptionSurvey}
setTitleSurvey={setTitleSurvey}
/>
<QuestionsList
questions={questions}
setQuestions={setQuestions}
surveyId={survey?.id}
/>
<SaveButton onClick={handleSave}/>
</div>
);
}

View file

@ -1,47 +1,39 @@
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';
const SurveyInfo: React.FC = () => {
const [descriptionSurvey, setDescriptionSurvey] = useState('');
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
interface SurveyInfoProps {
titleSurvey: string;
descriptionSurvey: string;
setDescriptionSurvey: (text: string) => void;
setTitleSurvey: (text: string) => void;
}
const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurvey, descriptionSurvey, setTitleSurvey}) => {
const [showDescriptionField, setShowDescriptionField] = useState(false);
const [showNewTitleField, setShowNewTitleField] = useState(false);
const titleTextareaRef = useRef<HTMLTextAreaElement>(null);
const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
const adjustTextareaHeight = (textarea: HTMLTextAreaElement | null) => {
if (textarea) {
// Сброс высоты перед расчетом
textarea.style.height = 'auto';
// Устанавливаем высоту равной scrollHeight + небольшой отступ
textarea.style.height = `${textarea.scrollHeight}px`;
// Центрируем содержимое вертикально
textarea.style.paddingTop = `${Math.max(0, (textarea.clientHeight - textarea.scrollHeight) / 2)}px`;
}
};
const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescriptionSurvey(descripEvent.target.value);
adjustTextareaHeight(descripEvent.target);
};
const handleNewTitleChange = (titleEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitleSurvey(titleEvent.target.value);
adjustTextareaHeight(titleEvent.target);
};
useEffect(() => {
if (showNewTitleField && titleTextareaRef.current) {
titleTextareaRef.current.focus();
adjustTextareaHeight(titleTextareaRef.current);
}
}, [showNewTitleField]);
useEffect(() => {
if (showDescriptionField && descriptionTextareaRef.current) {
descriptionTextareaRef.current.focus();
adjustTextareaHeight(descriptionTextareaRef.current);
}
}, [showDescriptionField]);
@ -91,16 +83,16 @@ const SurveyInfo: React.FC = () => {
} else if (showDescriptionField) {
return (
<div className={styles.descriptionWrapper}>
<textarea
ref={descriptionTextareaRef}
className={styles.textareaDescrip}
value={descriptionSurvey}
placeholder={'Добавить описание'}
onChange={handleDescriptionChange}
onKeyDown={handleDescriptionKeyDown}
onBlur={handleDescriptionBlur}
rows={1} // Начальное количество строк
/>
<TextareaAutosize
ref={descriptionTextareaRef}
className={styles.textareaDescrip}
value={descriptionSurvey}
placeholder={'Добавить описание'}
onChange={handleDescriptionChange}
onKeyDown={handleDescriptionKeyDown}
onBlur={handleDescriptionBlur}
rows={1}
/>
</div>
);
} else {
@ -122,7 +114,7 @@ const SurveyInfo: React.FC = () => {
{
showNewTitleField ? (
<h1 className={styles.titleSurvey}>
<textarea className={styles.textareaTitle}
<TextareaAutosize className={styles.textareaTitle}
ref={titleTextareaRef}
value={titleSurvey === 'Название опроса' ? '' : titleSurvey}
placeholder={'Название опроса'}

View file

@ -0,0 +1,61 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import SurveyInfo from '../SurveyInfo/SurveyInfo.tsx';
import QuestionsList, {Question} from '../QuestionsList/QuestionsList.tsx';
import {getSurveyById, ISurvey} from "../../api/SurveyApi.ts";
import {getListQuestions} from "../../api/QuestionApi.ts";
export const SurveyPage: React.FC = () => {
// const navigate = useNavigate();
const [survey, setSurvey] = useState<ISurvey | null>(null);
const [questions, setQuestions] = useState<Question[]>([]);
const [loading, setLoading] = useState(true);
const { id: surveyId } = useParams<{ id: string }>(); // Переименовываем для ясности
console.log(surveyId);
useEffect(() => {
if (!surveyId || isNaN(Number(surveyId))) {
console.error('Invalid survey ID');
return;
}
const fetchData = async () => {
try {
// Получаем опрос по surveyId
const surveyData = await getSurveyById(Number(surveyId));
setSurvey(surveyData);
// Получаем вопросы опроса по surveyId
const questionsData = await getListQuestions(Number(surveyId));
const formattedQuestions = questionsData.map(q => ({
id: q.id, // ID вопроса
text: q.title,
questionType: q.questionType,
}));
setQuestions(formattedQuestions as Question[]);
} catch (error) {
console.error('Ошибка:', error);
// navigate('/my-surveys');
}
};
fetchData();
}, [surveyId]);
if (loading) return <div>Загрузка...</div>;
if (!survey) return <div>Опрос не найден</div>;
return (
<div className="survey-page">
<SurveyInfo
titleSurvey={survey.title}
descriptionSurvey={survey.description}
setDescriptionSurvey={() => {}}
setTitleSurvey={() => {}}
/>
<QuestionsList
questions={questions}
setQuestions={() => {}}
surveyId={survey.id}
/>
</div>
);
};