From 5a1cc7c43cae4510192322ffafc1f4743be856be Mon Sep 17 00:00:00 2001 From: Tatiana Nikolaeva Date: Thu, 22 May 2025 20:23:09 +0500 Subject: [PATCH] api requests --- SurveyFrontend/package-lock.json | 143 ++++++++++++++++++ SurveyFrontend/package.json | 3 + SurveyFrontend/src/App.tsx | 1 + SurveyFrontend/src/api/AuthApi.ts | 41 ++++- SurveyFrontend/src/api/BaseApi.ts | 10 -- SurveyFrontend/src/api/QuestionApi.ts | 76 +++++++++- SurveyFrontend/src/api/SurveyApi.ts | 78 ++++++++-- SurveyFrontend/src/assets/react.svg | 1 - .../src/components/Account/Account.tsx | 39 ++++- .../src/components/Header/Header.tsx | 12 +- .../components/MySurveyList/MySurveyList.tsx | 68 +++++---- .../MySurveyList/MySurveysList.module.css | 27 ++++ .../src/components/Navigation/Navigation.tsx | 2 - .../components/QuestionItem/QuestionItem.tsx | 20 ++- .../QuestionsList/QuestionsList.tsx | 64 +++++--- .../src/components/Results/Results.tsx | 11 +- .../SaveButton/SaveButton.module.css | 5 +- .../src/components/SaveButton/SaveButton.tsx | 6 +- .../SettingSurvey/SettingSurvey.tsx | 12 +- .../src/components/Survey/Survey.tsx | 68 ++++++++- .../src/components/SurveyInfo/SurveyInfo.tsx | 50 +++--- .../src/components/SurveyPage/SurveyPage.tsx | 61 ++++++++ 22 files changed, 665 insertions(+), 133 deletions(-) delete mode 100644 SurveyFrontend/src/assets/react.svg create mode 100644 SurveyFrontend/src/components/SurveyPage/SurveyPage.tsx diff --git a/SurveyFrontend/package-lock.json b/SurveyFrontend/package-lock.json index d175002..432a9d5 100644 --- a/SurveyFrontend/package-lock.json +++ b/SurveyFrontend/package-lock.json @@ -9,9 +9,12 @@ "version": "0.0.0", "dependencies": { "@formkit/tempo": "^0.1.2", + "mobx": "^6.13.7", + "mobx-react": "^9.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.2", + "react-textarea-autosize": "^8.5.9", "uuid": "^11.1.0" }, "devDependencies": { @@ -251,6 +254,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", @@ -2785,6 +2797,66 @@ "node": "*" } }, + "node_modules/mobx": { + "version": "6.13.7", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", + "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-9.2.0.tgz", + "integrity": "sha512-dkGWCx+S0/1mfiuFfHRH8D9cplmwhxOV5CkXMp38u6rQGG2Pv3FWYztS0M7ncR6TyPRQKaTG/pnitInoYE9Vrw==", + "license": "MIT", + "dependencies": { + "mobx-react-lite": "^4.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/mobx-react-lite": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz", + "integrity": "sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3089,6 +3161,23 @@ "react-dom": ">=18" } }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3384,6 +3473,60 @@ "punycode": "^2.1.0" } }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/SurveyFrontend/package.json b/SurveyFrontend/package.json index 7f5fb7a..79c2b12 100644 --- a/SurveyFrontend/package.json +++ b/SurveyFrontend/package.json @@ -11,9 +11,12 @@ }, "dependencies": { "@formkit/tempo": "^0.1.2", + "mobx": "^6.13.7", + "mobx-react": "^9.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.2", + "react-textarea-autosize": "^8.5.9", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/SurveyFrontend/src/App.tsx b/SurveyFrontend/src/App.tsx index fa8d8ba..cdc3005 100644 --- a/SurveyFrontend/src/App.tsx +++ b/SurveyFrontend/src/App.tsx @@ -7,6 +7,7 @@ import {MySurveysPage} from "./pages/MySurveysPage/MySurveysPage.tsx"; 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"; const App = () => { return( diff --git a/SurveyFrontend/src/api/AuthApi.ts b/SurveyFrontend/src/api/AuthApi.ts index b46fff6..6592091 100644 --- a/SurveyFrontend/src/api/AuthApi.ts +++ b/SurveyFrontend/src/api/AuthApi.ts @@ -10,6 +10,45 @@ interface IRegistrationData extends IAuthData{ firstName: string; lastName: string; } +// +// interface IUserData{ +// username: string; +// firstName: string; +// lastName: string; +// email: string; +// } + +export const getCurrentUser = async (): Promise => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error("Токен отсутствует"); + } + + try { + const response = await fetch(`${BASE_URL}/auth/me`, { + ...createRequestConfig('GET'), + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.status === 401) { + localStorage.removeItem("token"); + throw new Error("Сессия истекла. Пожалуйста, войдите снова."); + } + + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + + const userData = await handleResponse(response); + localStorage.setItem("user", JSON.stringify(userData)); + return userData; + } catch (error) { + console.error("Ошибка при получении данных пользователя:", error); + throw error; + } +}; export const registerUser = async (data: IRegistrationData) => { try{ @@ -60,4 +99,4 @@ export const authUser = async (data: IAuthData) => { console.error("Login error:", error); throw error; } -}; \ No newline at end of file +}; diff --git a/SurveyFrontend/src/api/BaseApi.ts b/SurveyFrontend/src/api/BaseApi.ts index 3be4ee4..2041e4f 100644 --- a/SurveyFrontend/src/api/BaseApi.ts +++ b/SurveyFrontend/src/api/BaseApi.ts @@ -38,16 +38,6 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque * @param response Ответ от fetch * @returns Распарсенные данные или ошибку */ -// const handleResponse = async (response: Response) => { -// const data = await response.json(); -// -// if (!response.ok) { -// throw new Error(data.message || "Произошла ошибка"); -// } -// -// return data; -// }; - const handleResponse = async (response: Response) => { // Проверяем, есть ли контент в ответе const responseText = await response.text(); diff --git a/SurveyFrontend/src/api/QuestionApi.ts b/SurveyFrontend/src/api/QuestionApi.ts index ba3c2a8..0d80838 100644 --- a/SurveyFrontend/src/api/QuestionApi.ts +++ b/SurveyFrontend/src/api/QuestionApi.ts @@ -3,12 +3,17 @@ import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; export interface INewQuestion{ title: string; questionType: string; - answerVariants: string[]; } -// -// export interface IErrorQuestionResponse { -// -// } + +export interface IQuestion extends INewQuestion { + id: number; + surveyId: number; + answerVariants: Array<{ + id: number; + questionId: number; + text: string; + }> +} export const addNewQuestion = async (surveyId: number, question: INewQuestion) => { const token = localStorage.getItem("token"); @@ -27,3 +32,64 @@ export const addNewQuestion = async (surveyId: number, question: INewQuestion) = } } +export const getListQuestions = async (surveyId: number): Promise => { + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions`, { + ...createRequestConfig('GET'), + }) + if (response.status === 200) { + return await handleResponse(response); + } + throw new Error(`Ожидался код 200, получен ${response.status}`); + } + catch(error){ + console.error(`Error when receiving the list of questions: ${error}`); + throw error; + } +} + +export const updateQuestion = async (surveyId: number, id: number, question: Partial): Promise => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error("Токен отсутствует"); + } + + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${id}`, { + ...createRequestConfig('PUT'), + body: JSON.stringify({ + title: question.title, + questionType: question.questionType, + }), + }) + if (response.status === 200) { + return await handleResponse(response) + } + throw new Error(`Ожидался код 200, получен ${response.status}`) + } + catch(error){ + console.error(`Error when updating question: ${error}`); + throw error; + } +} + +export const deleteQuestion = async (surveyId: number, id: number) => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error("Токен отсутствует"); + } + + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${id}`, { + ...createRequestConfig('DELETE'), + }) + const responseData = await handleResponse(response); + if (response.ok && !responseData){ + return {success: true}; + } + return responseData; + } catch (error){ + console.error(`Error deleting a question: ${error}`); + throw error; + } +} \ No newline at end of file diff --git a/SurveyFrontend/src/api/SurveyApi.ts b/SurveyFrontend/src/api/SurveyApi.ts index 95b6032..bec3f70 100644 --- a/SurveyFrontend/src/api/SurveyApi.ts +++ b/SurveyFrontend/src/api/SurveyApi.ts @@ -51,29 +51,56 @@ export const getAllSurveys = async (): Promise => { * postNewSurvey - добавление нового опроса * @param survey */ -export const postNewSurvey = async (survey: INewSurvey): Promise => { +// export const postNewSurvey = async (survey: INewSurvey): Promise => { +// const token = localStorage.getItem("token"); +// if (!token) { +// throw new Error("Токен отсутствует"); +// } +// +// try{ +// const response = await fetch(`${BASE_URL}/surveys`, { +// ...createRequestConfig('POST'), +// body: JSON.stringify(survey) +// }) +// +// // return await handleResponse(response); +// +// if (response.status === 200) { +// return await handleResponse(response); +// } +// throw new Error(`Ожидался код 200, получен ${response.status}`); +// } catch (error) { +// console.error(`Error when adding a new survey: ${error}`); +// throw error; +// } +// } + +export const postNewSurvey = async (survey: INewSurvey): Promise => { const token = localStorage.getItem("token"); if (!token) { throw new Error("Токен отсутствует"); } - try{ + try { const response = await fetch(`${BASE_URL}/surveys`, { ...createRequestConfig('POST'), - body: JSON.stringify(survey) - }) + body: JSON.stringify(survey), + }); - // return await handleResponse(response); - - if (response.status === 201) { - return await handleResponse(response); + if (!response.ok) { + throw new Error(`Ошибка: ${response.status}`); } - throw new Error(`Ожидался код 201, получен ${response.status}`); + + const data = await response.json(); + if (!data.id) { + throw new Error("Сервер не вернул ID опроса"); + } + return data; } catch (error) { console.error(`Error when adding a new survey: ${error}`); throw error; } -} +}; /** * Запрос на получение опроса по заданному ID @@ -95,7 +122,7 @@ export const getSurveyById = async (surveyId: number): Promise => { * Запрос на удаление опроса * @param surveyId - ID выбранного опроса */ -export const deleteSurvey = async (surveyId: string) => { +export const deleteSurvey = async (surveyId: number) => { const token = localStorage.getItem("token"); if (!token) { throw new Error("Токен отсутствует"); @@ -114,4 +141,33 @@ export const deleteSurvey = async (surveyId: string) => { console.error(`Error deleting a survey: ${error}`); throw error; } +} + +/** + * Запрос на изменение опроса целиком + * @param surveyId + * @param survey + */ +export const updateSurvey = async (surveyId: number, survey: Partial): Promise => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error('Токен отсутствует'); + } + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}`, { + ...createRequestConfig('PUT'), + body: JSON.stringify({ + title: survey.title, + description: survey.description, + }) + }) + if (response.status === 200) { + return await handleResponse(response); + } + throw new Error(`Ожидался код 200, получен ${response.status}`); + } + catch (error){ + console.error(`Error updating survey: ${error}`); + throw error; + } } \ No newline at end of file diff --git a/SurveyFrontend/src/assets/react.svg b/SurveyFrontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/SurveyFrontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/SurveyFrontend/src/components/Account/Account.tsx b/SurveyFrontend/src/components/Account/Account.tsx index 9844f57..c618582 100644 --- a/SurveyFrontend/src/components/Account/Account.tsx +++ b/SurveyFrontend/src/components/Account/Account.tsx @@ -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 = ({href, user}) => { +const Account: React.FC = ({ href }) => { + const [userName, setUserName] = useState(); + 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
Загрузка...
; + } + return ( ); diff --git a/SurveyFrontend/src/components/Header/Header.tsx b/SurveyFrontend/src/components/Header/Header.tsx index 935bb34..a8ce9d8 100644 --- a/SurveyFrontend/src/components/Header/Header.tsx +++ b/SurveyFrontend/src/components/Header/Header.tsx @@ -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 = () => { - + ); }; -export default Header; +export default Header; \ No newline at end of file diff --git a/SurveyFrontend/src/components/MySurveyList/MySurveyList.tsx b/SurveyFrontend/src/components/MySurveyList/MySurveyList.tsx index 210996e..2694f4c 100644 --- a/SurveyFrontend/src/components/MySurveyList/MySurveyList.tsx +++ b/SurveyFrontend/src/components/MySurveyList/MySurveyList.tsx @@ -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(
{surveys.map((survey) => ( -
- + ))} ) diff --git a/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css b/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css index d7f500a..ec43e84 100644 --- a/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css +++ b/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css @@ -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; diff --git a/SurveyFrontend/src/components/Navigation/Navigation.tsx b/SurveyFrontend/src/components/Navigation/Navigation.tsx index 1a97ae9..0263fa6 100644 --- a/SurveyFrontend/src/components/Navigation/Navigation.tsx +++ b/SurveyFrontend/src/components/Navigation/Navigation.tsx @@ -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 = () => { ))} - ); }; diff --git a/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx b/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx index 2a52791..73c45b1 100644 --- a/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx +++ b/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx @@ -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; + selectedType: 'single' | 'multiply'; // Уточняем тип + setSelectedType: (type: 'single' | 'multiply') => void; // Уточняем тип } -const QuestionItem: React.FC = ({indexQuestion, initialTextQuestion = `Вопрос ${indexQuestion}`, - valueQuestion, onChangeQuestion, onDeleteQuestion}) => { - const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single'); +const QuestionItem: React.FC = ({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 = ({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) => { diff --git a/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx b/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx index 8c673dd..87fa255 100644 --- a/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx +++ b/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx @@ -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 = () => { - const [questions, setQuestions] = useState([ - { id: 1, text: '' }, - ]); +export interface Question { + id: number; + text: string; + questionType: 'singleanswerquestion' | 'multipleanswerquestion'; +} + +const QuestionsList: React.FC = ({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 = () => { 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 = () => { {questions.map((question) => ( handleDeleteQuestion(question.id)} onChangeQuestion={(value) => handleQuestionChange(question.id, value)} + selectedType={selectedType} + setSelectedType={setSelectedType} /> ))} diff --git a/SurveyFrontend/src/components/Results/Results.tsx b/SurveyFrontend/src/components/Results/Results.tsx index d21ab44..d41409c 100644 --- a/SurveyFrontend/src/components/Results/Results.tsx +++ b/SurveyFrontend/src/components/Results/Results.tsx @@ -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(
- +
) } \ No newline at end of file diff --git a/SurveyFrontend/src/components/SaveButton/SaveButton.module.css b/SurveyFrontend/src/components/SaveButton/SaveButton.module.css index c272122..e4cbd6b 100644 --- a/SurveyFrontend/src/components/SaveButton/SaveButton.module.css +++ b/SurveyFrontend/src/components/SaveButton/SaveButton.module.css @@ -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; -} \ No newline at end of file +} diff --git a/SurveyFrontend/src/components/SaveButton/SaveButton.tsx b/SurveyFrontend/src/components/SaveButton/SaveButton.tsx index 32396a9..2f8481a 100644 --- a/SurveyFrontend/src/components/SaveButton/SaveButton.tsx +++ b/SurveyFrontend/src/components/SaveButton/SaveButton.tsx @@ -2,12 +2,12 @@ import React from 'react' import styles from './SaveButton.module.css' interface CreateSurveyButtonProps { - // onClick(): void; + onClick(): void; } -const SaveButton: React.FC = () => { +const SaveButton: React.FC = ({onClick}) => { return ( - ); diff --git a/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx b/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx index e83f446..3e858f6 100644 --- a/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx +++ b/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx @@ -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 (
- +
diff --git a/SurveyFrontend/src/components/Survey/Survey.tsx b/SurveyFrontend/src/components/Survey/Survey.tsx index 7f96e12..8d9f5d8 100644 --- a/SurveyFrontend/src/components/Survey/Survey.tsx +++ b/SurveyFrontend/src/components/Survey/Survey.tsx @@ -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(null); + + const [questions, setQuestions] = useState([ + { 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 (
- - + + + +
); } diff --git a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx index a189cde..36e955c 100644 --- a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx +++ b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx @@ -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 = ({titleSurvey, setDescriptionSurvey, descriptionSurvey, setTitleSurvey}) => { const [showDescriptionField, setShowDescriptionField] = useState(false); const [showNewTitleField, setShowNewTitleField] = useState(false); const titleTextareaRef = useRef(null); const descriptionTextareaRef = useRef(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) => { setDescriptionSurvey(descripEvent.target.value); - adjustTextareaHeight(descripEvent.target); }; const handleNewTitleChange = (titleEvent: React.ChangeEvent) => { 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 (
-