Merge branch 'api-requests' into 'unstable'

Api requests

See merge request internship-2025/survey-webapp/survey-webapp!17
This commit is contained in:
Tatyana Nikolaeva 2025-05-24 11:48:10 +00:00
commit 4c4e6ee619
28 changed files with 833 additions and 189 deletions

View file

@ -9,9 +9,12 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@formkit/tempo": "^0.1.2", "@formkit/tempo": "^0.1.2",
"mobx": "^6.13.7",
"mobx-react": "^9.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.5.2", "react-router-dom": "^7.5.2",
"react-textarea-autosize": "^8.5.9",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -251,6 +254,15 @@
"@babel/core": "^7.0.0-0" "@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": { "node_modules/@babel/template": {
"version": "7.26.9", "version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
@ -2785,6 +2797,66 @@
"node": "*" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3089,6 +3161,23 @@
"react-dom": ">=18" "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": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -3384,6 +3473,60 @@
"punycode": "^2.1.0" "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": { "node_modules/uuid": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",

View file

@ -11,9 +11,12 @@
}, },
"dependencies": { "dependencies": {
"@formkit/tempo": "^0.1.2", "@formkit/tempo": "^0.1.2",
"mobx": "^6.13.7",
"mobx-react": "^9.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.5.2", "react-router-dom": "^7.5.2",
"react-textarea-autosize": "^8.5.9",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -7,6 +7,7 @@ import {MySurveysPage} from "./pages/MySurveysPage/MySurveysPage.tsx";
import {Results} from "./components/Results/Results.tsx"; import {Results} from "./components/Results/Results.tsx";
import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx"; import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx";
import AuthForm from "./pages/AuthForm/AuthForm.tsx"; import AuthForm from "./pages/AuthForm/AuthForm.tsx";
import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx";
const App = () => { const App = () => {
return( return(
@ -25,7 +26,7 @@ const App = () => {
</Route> </Route>
<Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}> <Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}>
<Route path="questions" element={<Survey />} /> <Route path="questions" element={<SurveyPage />} />
<Route path="settings" element={<SettingSurvey />} /> <Route path="settings" element={<SettingSurvey />} />
<Route path="results" element={<Results />} /> <Route path="results" element={<Results />} />
</Route> </Route>

View file

@ -10,6 +10,45 @@ interface IRegistrationData extends IAuthData{
firstName: string; firstName: string;
lastName: string; lastName: string;
} }
//
// interface IUserData{
// username: string;
// firstName: string;
// lastName: string;
// email: string;
// }
export const getCurrentUser = async (): Promise<IRegistrationData> => {
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) => { export const registerUser = async (data: IRegistrationData) => {
try{ try{
@ -40,7 +79,6 @@ export const authUser = async (data: IAuthData) => {
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
const responseData = await handleResponse(response); const responseData = await handleResponse(response);
console.log("Полный ответ сервера:", responseData);
const token = responseData.accessToken || responseData.token; const token = responseData.accessToken || responseData.token;
if (token) { if (token) {

View file

@ -38,16 +38,6 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque
* @param response Ответ от fetch * @param response Ответ от fetch
* @returns Распарсенные данные или ошибку * @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 handleResponse = async (response: Response) => {
// Проверяем, есть ли контент в ответе // Проверяем, есть ли контент в ответе
const responseText = await response.text(); const responseText = await response.text();

View file

@ -3,12 +3,17 @@ import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts";
export interface INewQuestion{ export interface INewQuestion{
title: string; title: string;
questionType: 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) => { export const addNewQuestion = async (surveyId: number, question: INewQuestion) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@ -27,3 +32,64 @@ export const addNewQuestion = async (surveyId: number, question: INewQuestion) =
} }
} }
export const getListQuestions = async (surveyId: number): Promise<IQuestion[]> => {
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<INewQuestion>): Promise<INewQuestion> => {
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;
}
}

View file

@ -51,29 +51,56 @@ export const getAllSurveys = async (): Promise<ISurvey[]> => {
* postNewSurvey - добавление нового опроса * postNewSurvey - добавление нового опроса
* @param survey * @param survey
*/ */
export const postNewSurvey = async (survey: INewSurvey): Promise<INewSurvey> => { // export const postNewSurvey = async (survey: INewSurvey): Promise<ISurvey> => {
// 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<ISurvey> => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (!token) { if (!token) {
throw new Error("Токен отсутствует"); throw new Error("Токен отсутствует");
} }
try{ try {
const response = await fetch(`${BASE_URL}/surveys`, { const response = await fetch(`${BASE_URL}/surveys`, {
...createRequestConfig('POST'), ...createRequestConfig('POST'),
body: JSON.stringify(survey) body: JSON.stringify(survey),
}) });
// return await handleResponse(response); if (!response.ok) {
throw new Error(`Ошибка: ${response.status}`);
if (response.status === 201) {
return await handleResponse(response);
} }
throw new Error(`Ожидался код 201, получен ${response.status}`);
const data = await response.json();
if (!data.id) {
throw new Error("Сервер не вернул ID опроса");
}
return data;
} catch (error) { } catch (error) {
console.error(`Error when adding a new survey: ${error}`); console.error(`Error when adding a new survey: ${error}`);
throw error; throw error;
} }
} };
/** /**
* Запрос на получение опроса по заданному ID * Запрос на получение опроса по заданному ID
@ -95,7 +122,7 @@ export const getSurveyById = async (surveyId: number): Promise<ISurvey> => {
* Запрос на удаление опроса * Запрос на удаление опроса
* @param surveyId - ID выбранного опроса * @param surveyId - ID выбранного опроса
*/ */
export const deleteSurvey = async (surveyId: string) => { export const deleteSurvey = async (surveyId: number) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (!token) { if (!token) {
throw new Error("Токен отсутствует"); throw new Error("Токен отсутствует");
@ -115,3 +142,32 @@ export const deleteSurvey = async (surveyId: string) => {
throw error; throw error;
} }
} }
/**
* Запрос на изменение опроса целиком
* @param surveyId
* @param survey
*/
export const updateSurvey = async (surveyId: number, survey: Partial<INewSurvey>): Promise<ISurvey> => {
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;
}
}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -1,18 +1,47 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import styles from './Account.module.css' import styles from './Account.module.css';
import AccountImg from '../../assets/account.svg?react'; import AccountImg from '../../assets/account.svg?react';
import { getCurrentUser } from '../../api/AuthApi';
interface AccountProps { interface AccountProps {
href: string; 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 ( return (
<div className={styles.account}> <div className={styles.account}>
<a className={styles.accountText} href={href}> <a className={styles.accountText} href={href}>
<AccountImg className={styles.accountImg}/> <AccountImg className={styles.accountImg}/>
{user} {userName}
</a> </a>
</div> </div>
); );

View file

@ -1,11 +1,9 @@
import React from "react"; import React from "react";
import Logo from "../Logo/Logo.tsx"; import Logo from "../Logo/Logo.tsx";
import Account from "../Account/Account.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"; import {Link, useLocation, useNavigate} from "react-router-dom";
const Header: React.FC = () => { const Header: React.FC = () => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -32,7 +30,7 @@ const Header: React.FC = () => {
{isMySurveysPage && <hr className={styles.activeLine}/>} {isMySurveysPage && <hr className={styles.activeLine}/>}
</Link> </Link>
</nav> </nav>
<Account href={'/profile'} user={'Иванов Иван'}/> <Account href={'/profile'} />
</div> </div>
); );
}; };

View file

@ -29,12 +29,30 @@
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; /* Цвет текста по умолчанию */ color: #000000;
outline: none; outline: none;
border: none; border: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.2); /* Нижняя граница с прозрачностью */ border-bottom: 2px solid rgba(0, 0, 0, 0.2);
padding: 5px 0; padding: 5px 0;
opacity: 1; /* Установите opacity в 1 для input, а для placeholder используйте opacity */ opacity: 1;
}
.password_container{
display: flex;
flex-direction: column;
gap: 3px;
}
.password_container .error{
border-bottom: 2px solid rgba(192, 35, 31, 1);
}
.errorMessage{
text-align: left;
font-size: 14px;
font-weight: 400;
line-height: 88%;
color: #C0231F;
} }
.input::placeholder { .input::placeholder {
@ -42,21 +60,20 @@
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; color: #000000;
opacity: 0.2; /* Прозрачность placeholder */ opacity: 0.2;
} }
.input:focus::placeholder { .input:focus::placeholder {
opacity: 0; /* Убираем placeholder при фокусе */ opacity: 0;
} }
/* Отключаем стиль для input, когда в нём есть данные */
.input:not(:placeholder-shown) { .input:not(:placeholder-shown) {
color: black; color: black;
opacity: 1; opacity: 1;
} }
.input:focus { .input:focus {
border-bottom: 1px solid black; /* Чёрная граница при фокусе */ border-bottom: 2px solid black;
} }
.signIn{ .signIn{

View file

@ -10,24 +10,33 @@ const LoginForm = () => {
}); });
const navigate = useNavigate(); const navigate = useNavigate();
const [error, setError] = useState<string | null>(null);
const emailRef = useRef<HTMLInputElement>(null); // ref для поля email const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null); // ref для поля password const passwordRef = useRef<HTMLInputElement>(null);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const email = emailRef.current?.value || ''; const email = emailRef.current?.value ?? '';
const password = passwordRef.current?.value || ''; const password = passwordRef.current?.value ?? '';
setError(null);
try{ try{
const responseData = await authUser({email, password}); const responseData = await authUser({email, password});
if (responseData && !responseData.error) if (responseData && !responseData.error)
navigate('/my-surveys'); navigate('/my-surveys');
else else
console.error('Ошибка аутентификации:', responseData); setError('Неверный логин или пароль')
} }
catch(err){ catch(err){
console.error('Ошибка при отправке запроса:', err); console.error('Ошибка при отправке запроса:', err);
setError('Неверный логин или пароль')
}
}
const handleChange = () => {
if (error) {
setError(null);
} }
} }
@ -40,19 +49,24 @@ const LoginForm = () => {
type={'email'} type={'email'}
placeholder='Почта' placeholder='Почта'
ref={emailRef} ref={emailRef}
onChange={handleChange}
onFocus={() => setFocused({ ...focused, email: true })} onFocus={() => setFocused({ ...focused, email: true })}
onBlur={() => setFocused({ ...focused, email: false })} onBlur={() => setFocused({ ...focused, email: false })}
style={{ color: focused.email ? 'black' : 'inherit' }} style={{ color: focused.email ? 'black' : 'inherit' }}
/> />
<div className={styles.password_container}>
<input <input
className={`${styles.input} ${styles.password}`} className={`${styles.input} ${styles.password} ${error ? styles.error : ''}`}
type='password' type='password'
placeholder='Пароль' placeholder='Пароль'
ref={passwordRef} ref={passwordRef}
onChange={handleChange}
onFocus={() => setFocused({ ...focused, password: true })} onFocus={() => setFocused({ ...focused, password: true })}
onBlur={() => setFocused({ ...focused, password: false })} onBlur={() => setFocused({ ...focused, password: false })}
style={{ color: focused.password ? 'black' : 'inherit' }} style={{ color: focused.password ? 'black' : 'inherit' }}
/> />
{error && <p className={styles.errorMessage}>{error}</p>}
</div>
<button className={styles.signIn} type="submit">Войти</button> <button className={styles.signIn} type="submit">Войти</button>
</form> </form>
<p className={styles.recommendation}>Еще не с нами? <p className={styles.recommendation}>Еще не с нами?

View file

@ -1,7 +1,8 @@
import styles from './MySurveysList.module.css' import styles from './MySurveysList.module.css'
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useEffect, useState} from "react"; 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{ interface MySurveyItem extends ISurvey{
@ -35,34 +36,40 @@ export const MySurveyList = () => {
fetchSurvey(); fetchSurvey();
}, [navigate]); }, [navigate]);
// const surveys: MySurveyItem[] = [ const handleSurveyClick = (surveyId: number) => {
// { navigate(`/survey/${surveyId}/questions`);
// 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 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( return(
<div className={styles.main}> <div className={styles.main}>
{surveys.map((survey) => ( {surveys.map((survey) => (
<button <div
key={survey.id} key={survey.id}
className={styles.survey} className={styles.survey}
onClick={() => handleSurveyClick(survey.id)} onClick={() => handleSurveyClick(survey.id)}
role='button'
tabIndex={0}
> >
<div className={styles.textContent}> <div className={styles.textContent}>
<div className={styles.surveyData}> <div className={styles.surveyData}>
@ -71,12 +78,21 @@ export const MySurveyList = () => {
</div> </div>
<span className={styles.date}>Дата создания: {survey.createdBy}</span> <span className={styles.date}>Дата создания: {survey.createdBy}</span>
</div> </div>
<div className={styles.container}>
<div className={`${styles.status} ${ <div className={`${styles.status} ${
survey.status === 'active' ? styles.active : styles.completed survey.status === 'active' ? styles.active : styles.completed
}`}> }`}>
{survey.status === 'active' ? 'Активен' : 'Завершён'} {survey.status === 'active' ? 'Активен' : 'Завершён'}
</div> </div>
<button
onClick={(e) => handleDeleteClick(survey.id, e)}
className={styles.buttonDelete}
>
Удалить опрос {' '}
<Delete className={styles.imgDelete}/>
</button> </button>
</div>
</div>
))} ))}
</div> </div>
) )

View file

@ -1,6 +1,7 @@
.main { .main {
background-color: #F6F6F6; background-color: #F6F6F6;
width: 100%; width: 100%;
max-width: 100vw;
min-height: 100vh; min-height: 100vh;
padding: 34px 10%; padding: 34px 10%;
} }
@ -26,6 +27,32 @@
flex-direction: column; 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 { .status {
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;

View file

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

View file

@ -7,16 +7,18 @@ import Delete from '../../assets/deleteQuestion.svg?react';
interface QuestionItemProps { interface QuestionItemProps {
indexQuestion: number; questionId: number;
initialTextQuestion?: string; initialTextQuestion?: string;
valueQuestion: string; valueQuestion: string;
onChangeQuestion: (valueQuestion: string) => void; 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}`, const QuestionItem: React.FC<QuestionItemProps> = ({questionId, initialTextQuestion = `Вопрос ${questionId}`,
valueQuestion, onChangeQuestion, onDeleteQuestion}) => { valueQuestion, onChangeQuestion, onDeleteQuestion, setSelectedType, selectedType}) => {
const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single'); // const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single');
const [answerOption, setAnswerOption] = useState(['']); const [answerOption, setAnswerOption] = useState(['']);
const [textQuestion, setTextQuestion] = useState(initialTextQuestion); const [textQuestion, setTextQuestion] = useState(initialTextQuestion);
const [isEditingQuestion, setIsEditingQuestion] = useState(false); const [isEditingQuestion, setIsEditingQuestion] = useState(false);
@ -86,8 +88,12 @@ const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQu
setTextQuestion(valueQuestion); setTextQuestion(valueQuestion);
}, [valueQuestion]); }, [valueQuestion]);
const handleDeleteQuestion = () => { const handleDeleteQuestion = async () => {
onDeleteQuestion(indexQuestion); try {
await onDeleteQuestion(questionId);
} catch (error) {
console.error('Ошибка при удалении вопроса:', error);
}
}; };
const toggleSelect = (index: number) => { 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 QuestionItem from "../QuestionItem/QuestionItem.tsx";
import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx"; import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx";
import {deleteQuestion} from "../../api/QuestionApi.ts";
interface QuestionsListProps {} interface QuestionsListProps {
questions: Question[];
interface Question { setQuestions: (questions: Question[]) => void;
id: number; surveyId?: number;
text: string;
} }
const QuestionsList: React.FC<QuestionsListProps> = () => { export interface Question {
const [questions, setQuestions] = useState<Question[]>([ id: number;
{ id: 1, text: '' }, 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 handleAddQuestion = () => {
const maxId = questions.reduce((max, question) => Math.max(max, question.id), 0);
const newQuestion: Question = { const newQuestion: Question = {
id: maxId + 1, id: localQuestionId,
text: '' text: '',
questionType: selectedType === 'single' ? 'singleanswerquestion' : 'multipleanswerquestion',
}; };
setQuestions([...questions, newQuestion]); 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 handleQuestionChange = (id: number, value: string) => {
const newQuestions = questions.map((question) => const newQuestions = questions.map((question) =>
question.id === id ? { ...question, text: value } : question question.id === id ? { ...question, text: value } : question
@ -30,9 +42,25 @@ const QuestionsList: React.FC<QuestionsListProps> = () => {
setQuestions(newQuestions); setQuestions(newQuestions);
}; };
const handleDeleteQuestion = (id: number) => { const handleDeleteQuestion = async (id: number) => {
const newQuestions = questions.filter((question) => question.id !== id); 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); setQuestions(newQuestions);
} catch (error) {
console.error('Ошибка при удалении вопроса:', error);
alert('Не удалось удалить вопрос: ' + (error instanceof Error ? error.message : 'Неизвестная ошибка'));
}
}; };
return ( return (
@ -40,10 +68,12 @@ const QuestionsList: React.FC<QuestionsListProps> = () => {
{questions.map((question) => ( {questions.map((question) => (
<QuestionItem <QuestionItem
key={question.id} key={question.id}
indexQuestion={question.id} questionId={question.id}
valueQuestion={question.text} valueQuestion={question.text}
onDeleteQuestion={() => handleDeleteQuestion(question.id)} onDeleteQuestion={() => handleDeleteQuestion(question.id)}
onChangeQuestion={(value) => handleQuestionChange(question.id, value)} onChangeQuestion={(value) => handleQuestionChange(question.id, value)}
selectedType={selectedType}
setSelectedType={setSelectedType}
/> />
))} ))}
<AddQuestionButton onClick={handleAddQuestion} /> <AddQuestionButton onClick={handleAddQuestion} />

View file

@ -28,12 +28,12 @@
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; /* Цвет текста по умолчанию */ color: #000000;
outline: none; outline: none;
border: none; border: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.2); /* Нижняя граница с прозрачностью */ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
padding: 5px 0; padding: 5px 0;
opacity: 1; /* Установите opacity в 1 для input, а для placeholder используйте opacity */ opacity: 1;
} }
.input::placeholder { .input::placeholder {
@ -41,11 +41,11 @@
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; color: #000000;
opacity: 0.2; /* Прозрачность placeholder */ opacity: 0.2;
} }
.input:focus::placeholder { .input:focus::placeholder {
opacity: 0; /* Убираем placeholder при фокусе */ opacity: 0;
} }
/* Отключаем стиль для input, когда в нём есть данные */ /* Отключаем стиль для input, когда в нём есть данные */
@ -82,3 +82,22 @@
color: #3788D6; color: #3788D6;
margin-left: 5px; margin-left: 5px;
} }
.emailContainer{
display: flex;
flex-direction: column;
gap: 3px;
}
.emailContainer .error{
border-bottom: 2px solid rgba(192, 35, 31, 1);
}
.errorMessage{
text-align: left;
font-size: 14px;
font-weight: 400;
line-height: 88%;
color: #C0231F;
margin: 0;
}

View file

@ -11,6 +11,8 @@ const RegisterForm = () => {
password: false password: false
}); });
const [error, setError] = useState<string | null>(null);
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
const surnameRef = useRef<HTMLInputElement>(null); const surnameRef = useRef<HTMLInputElement>(null);
const emailRef = useRef<HTMLInputElement>(null); const emailRef = useRef<HTMLInputElement>(null);
@ -20,15 +22,16 @@ const RegisterForm = () => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const firstName = nameRef.current?.value || ''; const firstName = nameRef.current?.value ?? '';
const lastName = surnameRef.current?.value || ''; const lastName = surnameRef.current?.value ?? '';
const email = emailRef.current?.value || ''; const email = emailRef.current?.value ?? '';
const password = passwordRef.current?.value || ''; const password = passwordRef.current?.value ?? '';
const username = firstName + lastName || ''; const username = firstName + lastName || '';
setError(null);
try{ try{
const responseData = await registerUser({username, firstName, lastName, email, password}); const responseData = await registerUser({username, firstName, lastName, email, password});
console.log(responseData); //проверка вывода данных
if (responseData && !responseData.error) { if (responseData && !responseData.error) {
console.log('Регистрация успешна'); console.log('Регистрация успешна');
localStorage.setItem("user", JSON.stringify({ localStorage.setItem("user", JSON.stringify({
@ -37,15 +40,28 @@ const RegisterForm = () => {
})); }));
navigate('/my-surveys'); navigate('/my-surveys');
} }
else { else if (responseData.status === 409){
console.error(`Ошибка регистрации: ${responseData}`); setError('Аккаунт с такой почтой уже зарегистрирован');
console.log('Регистраиця не удалась');
} }
} }
catch (err) { catch (err) {
console.error(`Ошибка при отправке запроса ${err}`); if (err instanceof Error) {
if (err.message.includes('409')) {
setError('Аккаунт с такой почтой уже зарегистрирован');
} else {
setError('Произошла ошибка при регистрации');
}
} else {
setError('Неизвестная ошибка');
} }
} }
}
const handleEmailChange = () => {
if (error) {
setError(null);
}
};
return ( return (
<div className={styles.registerContainer}> <div className={styles.registerContainer}>
@ -69,15 +85,19 @@ const RegisterForm = () => {
onBlur={() => setFocused({ ...focused, lastName: false })} onBlur={() => setFocused({ ...focused, lastName: false })}
style={{ color: focused.lastName ? 'black' : 'inherit' }} style={{ color: focused.lastName ? 'black' : 'inherit' }}
/> />
<div className={styles.emailContainer}>
<input <input
className={`${styles.input} ${styles.email}`} className={`${styles.input} ${styles.email} ${error ? styles.error : ''}`}
type={'email'} type={'email'}
placeholder='Почта' placeholder='Почта'
ref={emailRef} ref={emailRef}
onChange={handleEmailChange}
onFocus={() => setFocused({ ...focused, email: true })} onFocus={() => setFocused({ ...focused, email: true })}
onBlur={() => setFocused({ ...focused, email: false })} onBlur={() => setFocused({ ...focused, email: false })}
style={{ color: focused.email ? 'black' : 'inherit' }} style={{ color: focused.email ? 'black' : 'inherit' }}
/> />
{error && <p className={styles.errorMessage}>{error}</p>}
</div>
<input <input
className={`${styles.input} ${styles.password}`} className={`${styles.input} ${styles.password}`}
type='password' type='password'

View file

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

View file

@ -1,7 +1,8 @@
/*SaveButton.module.css*/ /*SaveButton.module.css*/
.createSurveyButton { .createSurveyButton {
margin-left: 40px; display: block;
margin: 10px auto;
padding: 25px 50.5px; padding: 25px 50.5px;
border: none; border: none;
border-radius: 20px; border-radius: 20px;

View file

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

View file

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

View file

@ -1,13 +1,77 @@
import React from "react"; import React, {useState} from "react";
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; 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 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 Survey: React.FC = () => {
const navigate = useNavigate();
const [descriptionSurvey, setDescriptionSurvey] = useState('');
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
const [survey] = 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 () => {
try {
const savedSurvey = await postNewSurvey({
title: titleSurvey,
description: descriptionSurvey
});
await Promise.all(
questions.map(question =>
addNewQuestion(savedSurvey.id, {
title: question.text,
questionType: question.questionType
})
)
);
navigate('/my-surveys');
} catch (error) {
console.error('Ошибка при сохранении:', error);
alert('Не удалось сохранить опрос');
}
};
return ( return (
<div className={styles.survey}> <div className={styles.survey}>
<SurveyInfo /> <SurveyInfo
<QuestionsList /> titleSurvey={titleSurvey}
descriptionSurvey={descriptionSurvey}
setDescriptionSurvey={setDescriptionSurvey}
setTitleSurvey={setTitleSurvey}
/>
<QuestionsList
questions={questions}
setQuestions={setQuestions}
surveyId={survey?.id}
/>
<SaveButton onClick={handleSave}/>
</div> </div>
); );
} }

View file

@ -1,47 +1,39 @@
import React, {useState, useRef, useEffect} from "react"; import React, {useState, useRef, useEffect} from "react";
import styles from './SurveyInfo.module.css' import styles from './SurveyInfo.module.css'
import AddDescripImg from '../../assets/add_circle.svg?react'; import AddDescripImg from '../../assets/add_circle.svg?react';
import TextareaAutosize from 'react-textarea-autosize';
const SurveyInfo: React.FC = () => {
const [descriptionSurvey, setDescriptionSurvey] = useState(''); interface SurveyInfoProps {
const [titleSurvey, setTitleSurvey] = useState('Название опроса'); 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 [showDescriptionField, setShowDescriptionField] = useState(false);
const [showNewTitleField, setShowNewTitleField] = useState(false); const [showNewTitleField, setShowNewTitleField] = useState(false);
const titleTextareaRef = useRef<HTMLTextAreaElement>(null); const titleTextareaRef = useRef<HTMLTextAreaElement>(null);
const descriptionTextareaRef = 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>) => { const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescriptionSurvey(descripEvent.target.value); setDescriptionSurvey(descripEvent.target.value);
adjustTextareaHeight(descripEvent.target);
}; };
const handleNewTitleChange = (titleEvent: React.ChangeEvent<HTMLTextAreaElement>) => { const handleNewTitleChange = (titleEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitleSurvey(titleEvent.target.value); setTitleSurvey(titleEvent.target.value);
adjustTextareaHeight(titleEvent.target);
}; };
useEffect(() => { useEffect(() => {
if (showNewTitleField && titleTextareaRef.current) { if (showNewTitleField && titleTextareaRef.current) {
titleTextareaRef.current.focus(); titleTextareaRef.current.focus();
adjustTextareaHeight(titleTextareaRef.current);
} }
}, [showNewTitleField]); }, [showNewTitleField]);
useEffect(() => { useEffect(() => {
if (showDescriptionField && descriptionTextareaRef.current) { if (showDescriptionField && descriptionTextareaRef.current) {
descriptionTextareaRef.current.focus(); descriptionTextareaRef.current.focus();
adjustTextareaHeight(descriptionTextareaRef.current);
} }
}, [showDescriptionField]); }, [showDescriptionField]);
@ -91,7 +83,7 @@ const SurveyInfo: React.FC = () => {
} else if (showDescriptionField) { } else if (showDescriptionField) {
return ( return (
<div className={styles.descriptionWrapper}> <div className={styles.descriptionWrapper}>
<textarea <TextareaAutosize
ref={descriptionTextareaRef} ref={descriptionTextareaRef}
className={styles.textareaDescrip} className={styles.textareaDescrip}
value={descriptionSurvey} value={descriptionSurvey}
@ -99,7 +91,7 @@ const SurveyInfo: React.FC = () => {
onChange={handleDescriptionChange} onChange={handleDescriptionChange}
onKeyDown={handleDescriptionKeyDown} onKeyDown={handleDescriptionKeyDown}
onBlur={handleDescriptionBlur} onBlur={handleDescriptionBlur}
rows={1} // Начальное количество строк rows={1}
/> />
</div> </div>
); );
@ -122,7 +114,7 @@ const SurveyInfo: React.FC = () => {
{ {
showNewTitleField ? ( showNewTitleField ? (
<h1 className={styles.titleSurvey}> <h1 className={styles.titleSurvey}>
<textarea className={styles.textareaTitle} <TextareaAutosize className={styles.textareaTitle}
ref={titleTextareaRef} ref={titleTextareaRef}
value={titleSurvey === 'Название опроса' ? '' : titleSurvey} value={titleSurvey === 'Название опроса' ? '' : titleSurvey}
placeholder={'Название опроса'} placeholder={'Название опроса'}

View file

@ -0,0 +1,10 @@
.survey_page{
width: 85%;
}
.error{
color: #C0231F;
text-align: center;
margin: 10px 0;
font-size: 18px;
}

View file

@ -0,0 +1,99 @@
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx";
import {useEffect, useState} from "react";
import {getSurveyById, ISurvey, updateSurvey} from "../../api/SurveyApi.ts";
import {useParams} from "react-router-dom";
import {getListQuestions} from "../../api/QuestionApi.ts";
import styles from "./SurveyPage.module.css";
import SaveButton from "../SaveButton/SaveButton.tsx";
export const SurveyPage: React.FC = () => {
const [survey, setSurvey] = useState<ISurvey | null>(null);
const [questions, setQuestions] = useState<Question[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { surveyId } = useParams<{ surveyId: string }>();
const [description, setDescription] = useState('');
const [title, setTitle] = useState('');
useEffect(() => {
if (!surveyId) {
console.error('Survey ID is missing');
return;
}
const id = parseInt(surveyId);
if (isNaN(id)) {
console.error('Invalid survey ID');
return;
}
const fetchData = async () => {
try {
setLoading(true);
const surveyData = await getSurveyById(id);
setSurvey(surveyData);
setTitle(surveyData.title);
setDescription(surveyData.description);
const questionsData = await getListQuestions(id);
const formattedQuestions = questionsData.map(q => ({
id: q.id,
text: q.title,
questionType: q.questionType as 'singleanswerquestion' | 'multipleanswerquestion',
}));
setQuestions(formattedQuestions);
} catch (error) {
console.error('Ошибка:', error);
setError('Не удалось загрузить опрос')
} finally {
setLoading(false);
}
};
fetchData();
}, [surveyId]);
if (loading) return <div>Загрузка...</div>;
if (!survey) return <div>Опрос не найден</div>;
const handleSave = async() => {
if (!surveyId || !survey) return;
try{
setError(null);
const id = parseInt(surveyId);
const surveyUpdated = await updateSurvey(id, {
title: title,
description: description,
})
setSurvey(surveyUpdated);
}
catch(error){
console.error('Ошибка при сохранении опроса:', error);
setError('Не удалось сохранить изменения');
throw error;
}
}
return (
<div className={styles.survey_page}>
<SurveyInfo
titleSurvey={title}
descriptionSurvey={description}
setDescriptionSurvey={setDescription}
setTitleSurvey={setTitle}
/>
<QuestionsList
questions={questions}
setQuestions={setQuestions}
surveyId={survey.id}
/>
{error && <div className={styles.error}>{error}</div>}
<SaveButton onClick={handleSave}/>
</div>
);
};

View file

@ -5,15 +5,6 @@ import {useLocation} from "react-router-dom";
const AuthForm = () => { const AuthForm = () => {
// const location = useLocation();
// const isLogin = location.pathname === '/login';
//
// return (
// <div className={`${isLogin ? styles.pageLogin : styles.page}`}>
// {isLogin ? <LoginForm /> : <RegisterForm/>}
// </div>
// );
const location = useLocation(); const location = useLocation();
const isLoginPage = location.pathname === '/login'; const isLoginPage = location.pathname === '/login';
const isRegisterPage = location.pathname === '/register'; const isRegisterPage = location.pathname === '/register';
@ -24,7 +15,7 @@ const AuthForm = () => {
} else if (isRegisterPage) { } else if (isRegisterPage) {
content = <RegisterForm />; content = <RegisterForm />;
} else { } else {
content = <LoginForm />; // По умолчанию показываем LoginForm content = <LoginForm />;
} }
return ( return (