Merge remote-tracking branch 'origin/unstable' into unstable
This commit is contained in:
commit
98468790ac
28 changed files with 833 additions and 189 deletions
143
SurveyFrontend/package-lock.json
generated
143
SurveyFrontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -25,7 +26,7 @@ const App = () => {
|
|||
</Route>
|
||||
|
||||
<Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}>
|
||||
<Route path="questions" element={<Survey />} />
|
||||
<Route path="questions" element={<SurveyPage />} />
|
||||
<Route path="settings" element={<SettingSurvey />} />
|
||||
<Route path="results" element={<Results />} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -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<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) => {
|
||||
try{
|
||||
|
|
@ -40,7 +79,6 @@ export const authUser = async (data: IAuthData) => {
|
|||
body: JSON.stringify(data),
|
||||
});
|
||||
const responseData = await handleResponse(response);
|
||||
console.log("Полный ответ сервера:", responseData);
|
||||
|
||||
const token = responseData.accessToken || responseData.token;
|
||||
if (token) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -51,29 +51,56 @@ export const getAllSurveys = async (): Promise<ISurvey[]> => {
|
|||
* postNewSurvey - добавление нового опроса
|
||||
* @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");
|
||||
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<ISurvey> => {
|
|||
* Запрос на удаление опроса
|
||||
* @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("Токен отсутствует");
|
||||
|
|
@ -115,3 +142,32 @@ export const deleteSurvey = async (surveyId: string) => {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,17 +20,17 @@ 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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,12 +29,30 @@
|
|||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 88%;
|
||||
color: #000000; /* Цвет текста по умолчанию */
|
||||
color: #000000;
|
||||
outline: 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;
|
||||
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 {
|
||||
|
|
@ -42,21 +60,20 @@
|
|||
font-weight: 600;
|
||||
line-height: 88%;
|
||||
color: #000000;
|
||||
opacity: 0.2; /* Прозрачность placeholder */
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.input:focus::placeholder {
|
||||
opacity: 0; /* Убираем placeholder при фокусе */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Отключаем стиль для input, когда в нём есть данные */
|
||||
.input:not(:placeholder-shown) {
|
||||
color: black;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-bottom: 1px solid black; /* Чёрная граница при фокусе */
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
|
||||
.signIn{
|
||||
|
|
|
|||
|
|
@ -10,24 +10,33 @@ const LoginForm = () => {
|
|||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const emailRef = useRef<HTMLInputElement>(null); // ref для поля email
|
||||
const passwordRef = useRef<HTMLInputElement>(null); // ref для поля password
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const email = emailRef.current?.value || '';
|
||||
const password = passwordRef.current?.value || '';
|
||||
const email = emailRef.current?.value ?? '';
|
||||
const password = passwordRef.current?.value ?? '';
|
||||
setError(null);
|
||||
|
||||
try{
|
||||
const responseData = await authUser({email, password});
|
||||
if (responseData && !responseData.error)
|
||||
navigate('/my-surveys');
|
||||
else
|
||||
console.error('Ошибка аутентификации:', responseData);
|
||||
setError('Неверный логин или пароль')
|
||||
}
|
||||
catch(err){
|
||||
console.error('Ошибка при отправке запроса:', err);
|
||||
setError('Неверный логин или пароль')
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = () => {
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,19 +49,24 @@ const LoginForm = () => {
|
|||
type={'email'}
|
||||
placeholder='Почта'
|
||||
ref={emailRef}
|
||||
onChange={handleChange}
|
||||
onFocus={() => setFocused({ ...focused, email: true })}
|
||||
onBlur={() => setFocused({ ...focused, email: false })}
|
||||
style={{ color: focused.email ? 'black' : 'inherit' }}
|
||||
/>
|
||||
<input
|
||||
className={`${styles.input} ${styles.password}`}
|
||||
type='password'
|
||||
placeholder='Пароль'
|
||||
ref={passwordRef}
|
||||
onFocus={() => setFocused({ ...focused, password: true })}
|
||||
onBlur={() => setFocused({ ...focused, password: false })}
|
||||
style={{ color: focused.password ? 'black' : 'inherit' }}
|
||||
/>
|
||||
<div className={styles.password_container}>
|
||||
<input
|
||||
className={`${styles.input} ${styles.password} ${error ? styles.error : ''}`}
|
||||
type='password'
|
||||
placeholder='Пароль'
|
||||
ref={passwordRef}
|
||||
onChange={handleChange}
|
||||
onFocus={() => setFocused({ ...focused, password: true })}
|
||||
onBlur={() => setFocused({ ...focused, password: false })}
|
||||
style={{ color: focused.password ? 'black' : 'inherit' }}
|
||||
/>
|
||||
{error && <p className={styles.errorMessage}>{error}</p>}
|
||||
</div>
|
||||
<button className={styles.signIn} type="submit">Войти</button>
|
||||
</form>
|
||||
<p className={styles.recommendation}>Еще не с нами?
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@
|
|||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 88%;
|
||||
color: #000000; /* Цвет текста по умолчанию */
|
||||
color: #000000;
|
||||
outline: 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;
|
||||
opacity: 1; /* Установите opacity в 1 для input, а для placeholder используйте opacity */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
|
|
@ -41,11 +41,11 @@
|
|||
font-weight: 600;
|
||||
line-height: 88%;
|
||||
color: #000000;
|
||||
opacity: 0.2; /* Прозрачность placeholder */
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.input:focus::placeholder {
|
||||
opacity: 0; /* Убираем placeholder при фокусе */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Отключаем стиль для input, когда в нём есть данные */
|
||||
|
|
@ -82,3 +82,22 @@
|
|||
color: #3788D6;
|
||||
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;
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ const RegisterForm = () => {
|
|||
password: false
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const surnameRef = useRef<HTMLInputElement>(null);
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -20,15 +22,16 @@ const RegisterForm = () => {
|
|||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const firstName = nameRef.current?.value || '';
|
||||
const lastName = surnameRef.current?.value || '';
|
||||
const email = emailRef.current?.value || '';
|
||||
const password = passwordRef.current?.value || '';
|
||||
const firstName = nameRef.current?.value ?? '';
|
||||
const lastName = surnameRef.current?.value ?? '';
|
||||
const email = emailRef.current?.value ?? '';
|
||||
const password = passwordRef.current?.value ?? '';
|
||||
const username = firstName + lastName || '';
|
||||
|
||||
setError(null);
|
||||
|
||||
try{
|
||||
const responseData = await registerUser({username, firstName, lastName, email, password});
|
||||
console.log(responseData); //проверка вывода данных
|
||||
if (responseData && !responseData.error) {
|
||||
console.log('Регистрация успешна');
|
||||
localStorage.setItem("user", JSON.stringify({
|
||||
|
|
@ -37,16 +40,29 @@ const RegisterForm = () => {
|
|||
}));
|
||||
navigate('/my-surveys');
|
||||
}
|
||||
else {
|
||||
console.error(`Ошибка регистрации: ${responseData}`);
|
||||
console.log('Регистраиця не удалась');
|
||||
else if (responseData.status === 409){
|
||||
setError('Аккаунт с такой почтой уже зарегистрирован');
|
||||
}
|
||||
}
|
||||
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 (
|
||||
<div className={styles.registerContainer}>
|
||||
<h2 className={styles.title}>Регистрация</h2>
|
||||
|
|
@ -69,15 +85,19 @@ const RegisterForm = () => {
|
|||
onBlur={() => setFocused({ ...focused, lastName: false })}
|
||||
style={{ color: focused.lastName ? 'black' : 'inherit' }}
|
||||
/>
|
||||
<input
|
||||
className={`${styles.input} ${styles.email}`}
|
||||
type={'email'}
|
||||
placeholder='Почта'
|
||||
ref={emailRef}
|
||||
onFocus={() => setFocused({ ...focused, email: true })}
|
||||
onBlur={() => setFocused({ ...focused, email: false })}
|
||||
style={{ color: focused.email ? 'black' : 'inherit' }}
|
||||
/>
|
||||
<div className={styles.emailContainer}>
|
||||
<input
|
||||
className={`${styles.input} ${styles.email} ${error ? styles.error : ''}`}
|
||||
type={'email'}
|
||||
placeholder='Почта'
|
||||
ref={emailRef}
|
||||
onChange={handleEmailChange}
|
||||
onFocus={() => setFocused({ ...focused, email: true })}
|
||||
onBlur={() => setFocused({ ...focused, email: false })}
|
||||
style={{ color: focused.email ? 'black' : 'inherit' }}
|
||||
/>
|
||||
{error && <p className={styles.errorMessage}>{error}</p>}
|
||||
</div>
|
||||
<input
|
||||
className={`${styles.input} ${styles.password}`}
|
||||
type='password'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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='Время окончания'/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,77 @@
|
|||
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] = 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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={'Название опроса'}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
.survey_page{
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.error{
|
||||
color: #C0231F;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
99
SurveyFrontend/src/components/SurveyPage/SurveyPage.tsx
Normal file
99
SurveyFrontend/src/components/SurveyPage/SurveyPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,15 +5,6 @@ import {useLocation} from "react-router-dom";
|
|||
|
||||
|
||||
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 isLoginPage = location.pathname === '/login';
|
||||
const isRegisterPage = location.pathname === '/register';
|
||||
|
|
@ -24,7 +15,7 @@ const AuthForm = () => {
|
|||
} else if (isRegisterPage) {
|
||||
content = <RegisterForm />;
|
||||
} else {
|
||||
content = <LoginForm />; // По умолчанию показываем LoginForm
|
||||
content = <LoginForm />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue