diff --git a/SurveyFrontend/src/App.tsx b/SurveyFrontend/src/App.tsx index 3f02788..27ff95c 100644 --- a/SurveyFrontend/src/App.tsx +++ b/SurveyFrontend/src/App.tsx @@ -9,7 +9,7 @@ import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx"; import AuthForm from "./pages/AuthForm/AuthForm.tsx"; import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx"; import CompleteSurvey from "./pages/CompleteSurvey/CompleteSurvey.tsx"; -import CompletingSurvey from "./components/CompletingSurvey/CompletingSurvey.tsx"; +import {SurveyProvider} from './context/SurveyContext.tsx'; const App = () => { return( @@ -19,8 +19,8 @@ const App = () => { } /> }> - } /> - } /> + } /> + } /> }> @@ -29,13 +29,11 @@ const App = () => { }> } /> - } /> + } /> } /> - }> - }/> - + }/> } /> diff --git a/SurveyFrontend/src/api/AnswerApi.ts b/SurveyFrontend/src/api/AnswerApi.ts index f29d5b4..d55ea44 100644 --- a/SurveyFrontend/src/api/AnswerApi.ts +++ b/SurveyFrontend/src/api/AnswerApi.ts @@ -1,94 +1,40 @@ -import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; +import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts"; -export interface INewAnswer{ - text: string; -} - -export interface IAnswerVariant extends INewAnswer{ - surveyId: number; - id: number; - questionId: number; -} - -export const getAnswerVariants = async (surveyId: number, questionId: number) => { - try{ - const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants`, { - ...createRequestConfig('GET') - }) - return await handleResponse(response) - }catch(err){ - console.error(`Error receiving response options: ${err}`); - throw err; - } -} - -export const addNewAnswerVariant = async (surveyId: number, questionId: number, answer: INewAnswer) => { - const token = localStorage.getItem("token"); - if (!token) { - throw new Error("Токен отсутствует"); - } - - try{ - const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants`, { - ...createRequestConfig('POST'), - body: JSON.stringify({ - text: answer.text, - }), - }) - - if (!response.ok) { - throw new Error(`Ошибка: ${response.status}`); - } - return await handleResponse(response) - } - catch(err){ - console.error(`Error adding a new response option: ${err}`); - throw err; - } -} - -export const updateAnswerVariant = async (surveyId: number, questionId: number, id: number, answer: INewAnswer): Promise => { - const token = localStorage.getItem("token"); - if (!token) { - throw new Error("Токен отсутствует"); - } - - try{ - const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants/${id}`, { - ...createRequestConfig('PUT'), - body: JSON.stringify({ - text: answer.text, - }) - }) - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error(`Ошибка ${response.status}: ${errorData?.message || 'Неизвестная ошибка'}`); - } - return await handleResponse(response) - } - catch(err){ - console.error(`Error updating the response option: ${err}`); - throw err; - } -} - -export const deleteAnswerVariant = async (surveyId: number, questionId: number, id: number) => { +export const getAnswer = async (id: number) => { const token = localStorage.getItem("token"); if (!token) { throw new Error('Токен отсутствует'); } + try{ - const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants/${id}`, { - ...createRequestConfig('DELETE'), + const response = await fetch(`${BASE_URL}/questions/${id}/answers`, { + ...createRequestConfig('GET'), }) - const responseData = await handleResponse(response); - if (response.ok && !responseData){ - return {success: true}; - } - return responseData; + return await handleResponse(response) } - catch(err){ - console.error(`Error deleting a answer: ${err}`); - throw err; + catch (error) { + handleUnauthorizedError(error); + console.error(`error when receiving the response: ${error}`); + throw error; + } +} + +export const getCompletionsAnswer = async (id: number) => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error('Токен отсутствует'); + } + + try{ + const response = await fetch(`${BASE_URL}/completions/${id}/answers`, { + ...createRequestConfig('GET'), + }) + + return await handleResponse(response) + } + catch (error) { + handleUnauthorizedError(error); + console.error(`error when receiving the selected response: ${error}`); + throw error; } } \ No newline at end of file diff --git a/SurveyFrontend/src/api/AnswerVariantsApi.ts b/SurveyFrontend/src/api/AnswerVariantsApi.ts new file mode 100644 index 0000000..5ba5a72 --- /dev/null +++ b/SurveyFrontend/src/api/AnswerVariantsApi.ts @@ -0,0 +1,97 @@ +import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts"; + +export interface INewAnswer{ + text: string; +} + +export interface IAnswerVariant extends INewAnswer{ + surveyId: number; + id: number; + questionId: number; +} + +export const getAnswerVariants = async (surveyId: number, questionId: number) => { + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants`, { + ...createRequestConfig('GET') + }) + return await handleResponse(response) + }catch(err){ + console.error(`Error receiving response options: ${err}`); + throw err; + } +} + +export const addNewAnswerVariant = async (surveyId: number, questionId: number, answer: INewAnswer) => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error("Токен отсутствует"); + } + + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants`, { + ...createRequestConfig('POST'), + body: JSON.stringify({ + text: answer.text, + }), + }) + + if (!response.ok) { + throw new Error(`Ошибка: ${response.status}`); + } + return await handleResponse(response) + } + catch(err){ + handleUnauthorizedError(err); + console.error(`Error adding a new response option: ${err}`); + throw err; + } +} + +export const updateAnswerVariant = async (id: number, answer: INewAnswer): Promise => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error("Токен отсутствует"); + } + + try{ + const response = await fetch(`${BASE_URL}/answerVariants/${id}`, { + ...createRequestConfig('PUT'), + body: JSON.stringify({ + text: answer.text, + }) + }) + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(`Ошибка ${response.status}: ${errorData?.message || 'Неизвестная ошибка'}`); + } + return await handleResponse(response) + } + catch(err){ + handleUnauthorizedError(err); + console.error(`Error updating the response option: ${err}`); + throw err; + } +} + +export const deleteAnswerVariant = async (id: number) => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error('Токен отсутствует'); + } + try{ + const response = await fetch(`${BASE_URL}/answerVariants/${id}`, { + ...createRequestConfig('DELETE'), + }) + const responseData = await handleResponse(response); + if (response.ok && !responseData){ + return {success: true}; + } + return responseData; + } + catch(err){ + handleUnauthorizedError(err); + console.error(`Error deleting a answer: ${err}`); + throw err; + } +} \ No newline at end of file diff --git a/SurveyFrontend/src/api/AuthApi.ts b/SurveyFrontend/src/api/AuthApi.ts index 749d189..c8f7b4e 100644 --- a/SurveyFrontend/src/api/AuthApi.ts +++ b/SurveyFrontend/src/api/AuthApi.ts @@ -1,4 +1,4 @@ -import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; +import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts"; interface IAuthData{ email: string; @@ -26,14 +26,6 @@ export const getCurrentUser = async () => { } }); - console.log(response); - - if (response.status === 401) { - localStorage.removeItem("token"); - localStorage.removeItem("user"); - throw new Error("Сессия истекла. Пожалуйста, войдите снова."); - } - if (!response.ok) { throw new Error(`Ошибка сервера: ${response.status}`); } @@ -42,33 +34,38 @@ export const getCurrentUser = async () => { localStorage.setItem("user", JSON.stringify(userData)); return userData; } catch (error) { + handleUnauthorizedError(error); console.error("Ошибка при получении данных пользователя:", error); throw error; } }; export const registerUser = async (data: IRegistrationData) => { - try{ + try { const response = await fetch(`${BASE_URL}/auth/register`, { - ...createRequestConfig('POST'), body: JSON.stringify(data), - }) - const responseData = await handleResponse(response); + ...createRequestConfig('POST'), + body: JSON.stringify(data), + }); - if (responseData.accessToken) { - localStorage.setItem("token", responseData.accessToken); - localStorage.setItem("user", JSON.stringify({ - firstName: data.firstName, - lastName: data.lastName, - email: data.email - })); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `Ошибка: ${response.status}`); + } + + const responseData = await handleResponse(response); + if (responseData.accessToken || responseData.token) { + localStorage.setItem("token", responseData.accessToken || responseData.token); + if (responseData.user) { + localStorage.setItem("user", JSON.stringify(responseData.user)); + } } return responseData; - } catch (error){ + } catch (error) { console.error("Registration error:", error); throw error; } -} +}; export const authUser = async (data: IAuthData) => { try { @@ -76,6 +73,7 @@ export const authUser = async (data: IAuthData) => { ...createRequestConfig('POST'), body: JSON.stringify(data), }); + const responseData = await handleResponse(response); const token = responseData.accessToken || responseData.token; @@ -91,6 +89,7 @@ export const authUser = async (data: IAuthData) => { return responseData; } catch (error) { + handleUnauthorizedError(error); console.error("Login error:", error); throw error; } diff --git a/SurveyFrontend/src/api/BaseApi.ts b/SurveyFrontend/src/api/BaseApi.ts index 6a85675..f1a537a 100644 --- a/SurveyFrontend/src/api/BaseApi.ts +++ b/SurveyFrontend/src/api/BaseApi.ts @@ -6,6 +6,13 @@ interface RequestConfig { body?: BodyInit | null; } +export const handleUnauthorizedError = (error: unknown) => { + if (error instanceof Error && error.message.includes('401')) { + window.location.href = '/login'; + console.log('Сессия истекла. Перенаправление на страницу входа.'); + } +}; + /** * Создаёт конфигурацию для fetch-запроса * @param method HTTP-метод (GET, POST, PUT, DELETE) @@ -37,14 +44,15 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque * @returns Распарсенные данные или ошибку */ const handleResponse = async (response: Response) => { + if (response.status === 401) { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + throw new Error("401: Unauthorized"); + } + const responseText = await response.text(); if (!responseText) { - if (response.status === 401) { - window.location.href = '/auth/login'; - throw new Error('Требуется авторизация'); - } - if (response.ok) { return null; } diff --git a/SurveyFrontend/src/api/CompletionApi.ts b/SurveyFrontend/src/api/CompletionApi.ts new file mode 100644 index 0000000..e5e5b60 --- /dev/null +++ b/SurveyFrontend/src/api/CompletionApi.ts @@ -0,0 +1,62 @@ +import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts"; +export interface ICompletionRequest { + answers: Array<{ + questionId: number; + answerText: string; + }>; +} + +export const getAllCompletions = async (surveyId: number) => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error('Токен отсутствует'); + } + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}/completions`, { + ...createRequestConfig('GET'), + }) + return await handleResponse(response); + } + catch (error) { + handleUnauthorizedError(error); + console.error(`Error when receiving all selected responses: ${error}`); + throw error; + } +} + +export const addNewCompletion = async (surveyId: number, data: ICompletionRequest) => { + try{ + const response = await fetch(`${BASE_URL}/surveys/${surveyId}/completions`, { + ...createRequestConfig('POST'), + body: JSON.stringify(data) + }) + if (!response.ok) { + throw new Error(`Ошибка: ${response.status}`); + } + return await handleResponse(response) + } + catch (error) { + handleUnauthorizedError(error); + console.error(`Error when adding a new survey passage: ${error}`); + throw error; + } +} + +export const getCompletionById = async (id: number) => { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error('Токен отсутствует'); + } + + try{ + const response = await fetch(`${BASE_URL}/completions/${id}`, { + ...createRequestConfig('GET'), + }) + return await handleResponse(response); + } + catch (error) { + handleUnauthorizedError(error); + console.error(`Error when receiving a completed survey by id: ${error}`); + throw error; + } +} \ No newline at end of file diff --git a/SurveyFrontend/src/api/ExportResultApi.ts b/SurveyFrontend/src/api/ExportResultApi.ts new file mode 100644 index 0000000..972a7fd --- /dev/null +++ b/SurveyFrontend/src/api/ExportResultApi.ts @@ -0,0 +1,42 @@ +import {BASE_URL, createRequestConfig, handleUnauthorizedError} from "./BaseApi.ts"; + +export const getResultsFile = async (surveyId: number) => { + try { + const response = await fetch(`${BASE_URL}/export/excel/${surveyId}`, { + ...createRequestConfig('GET'), + }); + + if (!response.ok) { + throw new Error('Ошибка при получении файла'); + } + + const blob = await response.blob(); + + const url = window.URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + + const contentDisposition = response.headers.get('content-disposition'); + let fileName = `survey_results_${surveyId}.xlsx`; + + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/); + if (fileNameMatch && fileNameMatch[1]) { + fileName = fileNameMatch[1]; + } + } + + a.download = fileName; + document.body.appendChild(a); + a.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } + catch (error) { + handleUnauthorizedError(error); + console.error(`Error when receiving survey results: ${error}`); + throw error; + } +} \ No newline at end of file diff --git a/SurveyFrontend/src/api/QuestionApi.ts b/SurveyFrontend/src/api/QuestionApi.ts index 90593b6..d16668c 100644 --- a/SurveyFrontend/src/api/QuestionApi.ts +++ b/SurveyFrontend/src/api/QuestionApi.ts @@ -1,5 +1,5 @@ -import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; -import {IAnswerVariant} from "./AnswerApi.ts"; +import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts"; +import {IAnswerVariant} from "./AnswerVariantsApi.ts"; export interface INewQuestion{ title: string; @@ -25,6 +25,7 @@ export const addNewQuestion = async (surveyId: number, question: INewQuestion) = }) return await handleResponse(response) } catch (error){ + handleUnauthorizedError(error); throw new Error(`Error when adding a new question: ${error}`); } } @@ -34,50 +35,46 @@ export const getListQuestions = async (surveyId: number): Promise = 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}`); + return await handleResponse(response); } catch(error){ + handleUnauthorizedError(error); console.error(`Error when receiving the list of questions: ${error}`); throw error; } } -export const updateQuestion = async (surveyId: number, id: number, question: Partial): Promise => { +export const updateQuestion = async (id: number, question: Partial): Promise => { const token = localStorage.getItem("token"); if (!token) { throw new Error("Токен отсутствует"); } try{ - const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${id}`, { + const response = await fetch(`${BASE_URL}/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}`) + return await handleResponse(response) } catch(error){ + handleUnauthorizedError(error); console.error(`Error when updating question: ${error}`); throw error; } } -export const deleteQuestion = async (surveyId: number, id: number) => { +export const deleteQuestion = async (id: number) => { const token = localStorage.getItem("token"); if (!token) { throw new Error("Токен отсутствует"); } try{ - const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${id}`, { + const response = await fetch(`${BASE_URL}/questions/${id}`, { ...createRequestConfig('DELETE'), }) const responseData = await handleResponse(response); @@ -86,6 +83,7 @@ export const deleteQuestion = async (surveyId: number, id: number) => { } return responseData; } catch (error){ + handleUnauthorizedError(error); console.error(`Error deleting a question: ${error}`); throw error; } diff --git a/SurveyFrontend/src/api/SurveyApi.ts b/SurveyFrontend/src/api/SurveyApi.ts index 4c20c73..ad3baab 100644 --- a/SurveyFrontend/src/api/SurveyApi.ts +++ b/SurveyFrontend/src/api/SurveyApi.ts @@ -1,4 +1,4 @@ -import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; +import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts"; export interface ISurvey { id: number; @@ -28,6 +28,7 @@ export const getMySurveys = async (): Promise => { }); return await handleResponse(response); } catch (error) { + handleUnauthorizedError(error); console.error("Error receiving surveys:", error); throw error; } @@ -43,6 +44,7 @@ export const getAllSurveys = async (): Promise => { }) return await handleResponse(response); } catch (error) { + handleUnauthorizedError(error); console.error("Error receiving surveys:", error); throw error; } @@ -68,12 +70,13 @@ export const postNewSurvey = async (survey: INewSurvey): Promise => { throw new Error(`Ошибка: ${response.status}`); } - const data = await response.json(); + const data = await handleResponse(response); if (!data.id) { throw new Error("Сервер не вернул ID опроса"); } return data; } catch (error) { + handleUnauthorizedError(error); console.error(`Error when adding a new survey: ${error}`); throw error; } @@ -90,6 +93,7 @@ export const getSurveyById = async (surveyId: number): Promise => { }) return await handleResponse(response); } catch (error){ + handleUnauthorizedError(error); console.error(`Error finding the survey by id: ${error}`); throw error; } @@ -115,6 +119,7 @@ export const deleteSurvey = async (surveyId: number) => { } return responseData; } catch (error){ + handleUnauthorizedError(error); console.error(`Error deleting a survey: ${error}`); throw error; } @@ -138,12 +143,10 @@ export const updateSurvey = async (surveyId: number, survey: Partial description: survey.description, }) }) - if (response.status === 200) { - return await handleResponse(response); - } - throw new Error(`Ожидался код 200, получен ${response.status}`); + return await handleResponse(response); } catch (error){ + handleUnauthorizedError(error); console.error(`Error updating survey: ${error}`); throw error; } diff --git a/SurveyFrontend/src/components/Account/Account.module.css b/SurveyFrontend/src/components/Account/Account.module.css index 20e0844..72c52de 100644 --- a/SurveyFrontend/src/components/Account/Account.module.css +++ b/SurveyFrontend/src/components/Account/Account.module.css @@ -4,8 +4,8 @@ background-color: #F3F3F3; border-radius: 40px; align-items: center; - padding: 4.58px 13px 4.58px 4.58px; - margin: 26px 33px 27px 0; + padding: 2px 10px 2px 4.5px; + margin: 15px 33px 15px 0; margin-left: auto; display: inline-flex; max-width: 100%; @@ -18,7 +18,7 @@ display: flex; align-items: center; justify-content: space-around; - font-size: 24px; + font-size: 18px; font-weight: 600; color: black; text-decoration: none; @@ -27,7 +27,6 @@ .accountImg{ vertical-align: middle; - width: 55px; - margin-right: 9px; + width: 40px; flex-shrink: 0; } diff --git a/SurveyFrontend/src/components/Account/Account.tsx b/SurveyFrontend/src/components/Account/Account.tsx index 3f93837..4728669 100644 --- a/SurveyFrontend/src/components/Account/Account.tsx +++ b/SurveyFrontend/src/components/Account/Account.tsx @@ -11,28 +11,6 @@ const Account: React.FC = ({ href }) => { const [userName, setUserName] = useState(); const [isLoading, setIsLoading] = useState(true); - // useEffect(() => { - // const fetchUserData = async () => { - // try { - // const userData = localStorage.getItem("user"); - // - // if (userData) { - // const parsedData = JSON.parse(userData); - // setUserName(`${parsedData.firstName} ${parsedData.lastName}`); - // } else { - // const data = await getCurrentUser(); - // setUserName(`${data.firstName} ${data.lastName}`); - // } - // } catch (error) { - // console.error("Ошибка загрузки данных пользователя:", error); - // } finally { - // setIsLoading(false); - // } - // }; - // - // fetchUserData(); - // }, []); - useEffect(() => { const fetchUserData = async () => { try { diff --git a/SurveyFrontend/src/components/AddAnswerButton/AddAnswerButton.module.css b/SurveyFrontend/src/components/AddAnswerButton/AddAnswerButton.module.css index 297d221..25e6de6 100644 --- a/SurveyFrontend/src/components/AddAnswerButton/AddAnswerButton.module.css +++ b/SurveyFrontend/src/components/AddAnswerButton/AddAnswerButton.module.css @@ -3,15 +3,16 @@ .answerButton { margin-top: 18px; display: flex; - gap: 10px; + gap: 8px; align-items: center; border: none; background-color: white; color: #3788D6; - font-size: 18px; + font-size: 15px; font-weight: 500; } .addAnswerImg{ + width: 12px; vertical-align: middle; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/AddQuestionButton/AddQuestionButton.module.css b/SurveyFrontend/src/components/AddQuestionButton/AddQuestionButton.module.css index 7326604..69f5a71 100644 --- a/SurveyFrontend/src/components/AddQuestionButton/AddQuestionButton.module.css +++ b/SurveyFrontend/src/components/AddQuestionButton/AddQuestionButton.module.css @@ -4,7 +4,7 @@ margin: 0 auto; display: flex; flex-direction: column; - margin-bottom: 80px; + margin-bottom: 40px; align-items: center; background-color: #F6F6F6; border: none; @@ -12,12 +12,13 @@ } .questionButtonImg{ - width: 54px; + width: 40px; align-items: center; + margin-bottom: -15px; } .textButton{ - font-size: 24px; + font-size: 18px; font-weight: 600; text-align: center; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/AnswerOption/AnswerOption.module.css b/SurveyFrontend/src/components/AnswerOption/AnswerOption.module.css index 9f0ce82..8e2c434 100644 --- a/SurveyFrontend/src/components/AnswerOption/AnswerOption.module.css +++ b/SurveyFrontend/src/components/AnswerOption/AnswerOption.module.css @@ -4,19 +4,20 @@ width: 100%; display: flex; gap: 10px; - margin-bottom: 17px; + margin-bottom: 7px; align-items: flex-start; } .textAnswer { text-align: left; border: none; + outline: none; background: none; - font-size: 18px; + font-size: 15px; font-weight: 500; word-break: break-word; - width: 70%; padding: 0; + margin-right: 150px; line-height: 24px; cursor: text; margin-top: 2px; @@ -38,20 +39,21 @@ } .answerIcon { - width: 24px; - height: 24px; + width: 20px; + height: 20px; display: block; } .answerInput { - font-size: 18px; + font-size: 15px; font-weight: 500; outline: none; border: none; resize: none; width: 70%; padding: 0; - margin-top: 2px; + margin-top: 3px; + margin-bottom: -20px; font-family: inherit; min-height: 24px; height: auto; @@ -66,4 +68,8 @@ border: none; background-color: transparent; padding: 0; +} + +.img{ + width: 20px; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx b/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx index 1bf5bb5..79706ab 100644 --- a/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx +++ b/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx @@ -6,6 +6,7 @@ import Multiple from '../../assets/emptyCheckbox.svg?react'; import SelectedSingle from '../../assets/radio_button_checked.svg?react' import SelectedMultiple from '../../assets/check_box.svg?react'; import TextareaAutosize from 'react-textarea-autosize'; +import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts"; interface AnswerOptionProps{ index: number; @@ -15,14 +16,14 @@ interface AnswerOptionProps{ selectedType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion'; isSelected?: boolean; toggleSelect?: () => void; - isCompleteSurveyActive?: boolean; } -const AnswerOption: React.FC = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect, isCompleteSurveyActive = false}) => { +const AnswerOption: React.FC = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect}) => { const [currentValue, setCurrentValue] = useState(value); const [isEditing, setIsEditing] = useState(false); const textAreaRef = useRef(null); + const isReadOnly = useRouteReadOnly(); useEffect(() => { setCurrentValue(value); @@ -71,7 +72,7 @@ const AnswerOption: React.FC = ({index, value, onChange, onDe }, [isEditing]); const handleMarkerClick = () => { - if (isCompleteSurveyActive && toggleSelect) { + if (isReadOnly && toggleSelect) { toggleSelect(); } }; @@ -79,7 +80,7 @@ const AnswerOption: React.FC = ({index, value, onChange, onDe return (
- {isCompleteSurveyActive ? ( + {isReadOnly ? ( )} - {isCompleteSurveyActive ? ( + {isReadOnly ? ( @@ -128,9 +129,9 @@ const AnswerOption: React.FC = ({index, value, onChange, onDe )} - {!isCompleteSurveyActive && ( + {!isReadOnly && ( )}
diff --git a/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.module.css b/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.module.css index aa10c8d..c5fa2b0 100644 --- a/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.module.css +++ b/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.module.css @@ -4,4 +4,19 @@ max-width: 100vw; min-height: 100vh; padding: 34px 16%; +} + +.departur_button{ + display: block; + margin: 10px auto; + padding: 20px 40px; + border: none; + border-radius: 20px; + background-color: #3788D6; + color: white; + font-weight: 700; + font-size: 18px; + text-align: center; + box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1); + box-sizing: border-box; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx b/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx index 69eb87f..2825a3c 100644 --- a/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx +++ b/SurveyFrontend/src/components/CompletingSurvey/CompletingSurvey.tsx @@ -1,30 +1,117 @@ import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx"; -import {useState} from "react"; +import {useEffect, useState} from "react"; import styles from './CompletingSurvey.module.css' +import {useNavigate, useParams} from "react-router-dom"; +import {getSurveyById, ISurvey} from "../../api/SurveyApi.ts"; +import {getListQuestions} from "../../api/QuestionApi.ts"; +import {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts"; +import {addNewCompletion} from "../../api/CompletionApi.ts"; + +interface ISelectedAnswers{ + questionId: number; + answerText: string; +} export const CompletingSurvey = () => { - const [titleSurvey, setTitleSurvey] = useState("Название опроса"); - const [descriptionSurvey, setDescriptionSurvey] = useState(""); - const [questions, setQuestions] = useState([ - { id: 1, text: 'Вопрос 1', questionType: 'SingleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' }, - { id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]}, - { id: 2, text: 'Вопрос 2', questionType: 'MultipleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' }, - { id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]} - ]); + const {surveyId} = useParams<{surveyId: string}>(); + const [survey, setSurvey] = useState(null); + const [questions, setQuestions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [selectedAnswers, setSelectedAnswers] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + const fetchSurveyData = async () => { + try { + setLoading(true); + if (!surveyId) return; + + const surveyData = await getSurveyById(parseInt(surveyId)); + setSurvey(surveyData); + + const questionsData = await getListQuestions(parseInt(surveyId)); + const formattedQuestions = await Promise.all(questionsData.map(async q => { + const answerVariants = await getAnswerVariants(parseInt(surveyId), q.id); + return { + id: q.id, + text: q.title, + questionType: q.questionType as 'SingleAnswerQuestion' | 'MultipleAnswerQuestion', + answerVariants: answerVariants.map((a: IAnswerVariant) => ({ + id: a.id, + text: a.text + })) + }; + })); + setQuestions(formattedQuestions); + } catch (error) { + console.error('Ошибка загрузки опроса:', error); + setError('Не удалось загрузить опрос'); + } finally { + setLoading(false); + } + }; + + fetchSurveyData(); + }, [surveyId]); + + const handleAnswerSelect = (questionId: number, answerText: string) => { + setSelectedAnswers(prev => { + const question = questions.find(q => q.id === questionId); + if (question?.questionType === 'SingleAnswerQuestion') { + return [ + ...prev.filter(a => a.questionId !== questionId), + { questionId, answerText } + ]; + } + + const existingAnswerIndex = prev.findIndex( + a => a.questionId === questionId && a.answerText === answerText + ); + + if (existingAnswerIndex >= 0) { + return prev.filter((_, index) => index !== existingAnswerIndex); + } else { + return [...prev, { questionId, answerText }]; + } + }); + }; + + const handleSubmit = async () => { + if (!surveyId) return; + + try { + await addNewCompletion(parseInt(surveyId), { + answers: selectedAnswers + }); + + navigate('/my-surveys'); + } catch (error) { + console.error('Ошибка при отправке ответов:', error); + } + }; + + if (loading) return
Загрузка...
; + if (error) return
{error}
; + if (!survey) return
Опрос не найден
; + return (
setSurvey({ ...survey, description: value })} + setTitleSurvey={(value) => setSurvey({ ...survey, title: value })} /> +
) } diff --git a/SurveyFrontend/src/components/Header/Header.module.css b/SurveyFrontend/src/components/Header/Header.module.css index 27744e1..add3c79 100644 --- a/SurveyFrontend/src/components/Header/Header.module.css +++ b/SurveyFrontend/src/components/Header/Header.module.css @@ -5,18 +5,19 @@ padding: 0; width: 100%; display: flex; + height: fit-content; } .pagesNav{ display: flex; - gap: 60px; + gap: 80px; list-style: none; align-items: center; margin-right: 20%; } .pageLink{ - font-size: 24px; + font-size: 18px; font-weight: 600; color: #2A6DAE; padding: 0; diff --git a/SurveyFrontend/src/components/Header/Header.tsx b/SurveyFrontend/src/components/Header/Header.tsx index 6332955..e90f0e7 100644 --- a/SurveyFrontend/src/components/Header/Header.tsx +++ b/SurveyFrontend/src/components/Header/Header.tsx @@ -10,13 +10,12 @@ const Header: React.FC = () => { const isCreateSurveyActive = location.pathname.startsWith('/survey/create'); const isMySurveysActive = location.pathname === '/my-surveys'; - const isCompleteSurveyActive = location.pathname === '/complete-survey'; const isSurveyViewPage = location.pathname.startsWith('/survey/') && !location.pathname.startsWith('/survey/create'); const handleLogoClick = () => { - navigate(location.pathname, { replace: true }); + navigate(location.pathname); }; return ( @@ -37,13 +36,6 @@ const Header: React.FC = () => { Мои опросы {(isMySurveysActive || isSurveyViewPage) &&
} - - Прохождение опроса - {isCompleteSurveyActive &&
} - diff --git a/SurveyFrontend/src/components/LoginForm/LoginForm.module.css b/SurveyFrontend/src/components/LoginForm/LoginForm.module.css index d3f041b..bb37532 100644 --- a/SurveyFrontend/src/components/LoginForm/LoginForm.module.css +++ b/SurveyFrontend/src/components/LoginForm/LoginForm.module.css @@ -1,19 +1,19 @@ .loginContainer{ - width: 31%; + width: 26%; + height: fit-content; background-color: #FFFFFF; padding: 42.5px 65px; - margin: auto; + margin: 0 auto; border-radius: 43px; - margin-bottom: 0; } .title{ text-align: center; font-weight: 600; - font-size: 40px; + font-size: 30px; line-height: 88%; padding: 0; - margin-bottom: 80px; + margin-bottom: 60px; margin-top: 0; } @@ -21,12 +21,12 @@ text-align: center; display: flex; flex-direction: column; - gap: 80px; - margin-bottom: 80px; + gap: 50px; + margin-bottom: 40px; } .input { - font-size: 24px; + font-size: 18px; font-weight: 600; line-height: 88%; color: #000000; @@ -49,14 +49,14 @@ .errorMessage{ text-align: left; - font-size: 14px; + font-size: 12px; font-weight: 400; line-height: 88%; color: #C0231F; } .input::placeholder { - font-size: 24px; + font-size: 18px; font-weight: 600; line-height: 88%; color: #000000; @@ -78,12 +78,12 @@ .signIn{ margin: auto; - padding: 26.5px 67px; + padding: 20px 40px; width: fit-content; border-radius: 24px; background-color: #3788D6; color: #FFFFFF; - font-size: 24px; + font-size: 20px; font-weight: 600; line-height: 120%; border: none; @@ -92,7 +92,7 @@ .recommendation{ text-align: center; - font-size: 18px; + font-size: 15px; font-weight: 500; } diff --git a/SurveyFrontend/src/components/LoginForm/LoginForm.tsx b/SurveyFrontend/src/components/LoginForm/LoginForm.tsx index 8431a04..b397b34 100644 --- a/SurveyFrontend/src/components/LoginForm/LoginForm.tsx +++ b/SurveyFrontend/src/components/LoginForm/LoginForm.tsx @@ -27,7 +27,7 @@ const LoginForm = () => { else { const responseData = await authUser({email, password}); if (responseData && !responseData.error) - navigate('/my-surveys'); + navigate('/my-surveys', {replace: true}); else setError('Неверный логин или пароль') } diff --git a/SurveyFrontend/src/components/Logo/Logo.module.css b/SurveyFrontend/src/components/Logo/Logo.module.css index 8a28fbf..b8e2295 100644 --- a/SurveyFrontend/src/components/Logo/Logo.module.css +++ b/SurveyFrontend/src/components/Logo/Logo.module.css @@ -1,8 +1,13 @@ /*Logo.module.css*/ .logo { + outline: none; padding: 0; - height: 52px; - width: 52px; - margin: 31px 77px 25px 40px; + margin: 0 100px 0 40px; + display: flex; + align-items: center; +} +.logoImg{ + outline: none; + width: 40px; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/Logo/Logo.tsx b/SurveyFrontend/src/components/Logo/Logo.tsx index 529b00b..29f5e36 100644 --- a/SurveyFrontend/src/components/Logo/Logo.tsx +++ b/SurveyFrontend/src/components/Logo/Logo.tsx @@ -10,7 +10,7 @@ interface LogoProps { const Logo: React.FC = ({href, onClick}) => { return ( - + ); }; diff --git a/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css b/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css index 1ca9d3c..3cc4951 100644 --- a/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css +++ b/SurveyFrontend/src/components/MySurveyList/MySurveysList.module.css @@ -12,7 +12,7 @@ background-color: white; width: 79%; border-radius: 14px; - padding: 29px 36px 29px 54px; + padding: 29px 36px 15px 54px; margin-bottom: 23px; gap: 20px; border: none; @@ -35,6 +35,8 @@ .buttonDelete{ border-radius: 8px; + margin-top: 10px; + margin-left: 30px; align-items: center; background-color: #FFFFFF; border: none; @@ -42,7 +44,7 @@ padding: 5px 3px; color: black; font-weight: 500; - font-size: 18px; + font-size: 15px; } .buttonDelete:hover{ @@ -51,12 +53,13 @@ .imgDelete{ vertical-align: middle; + width: 18px; } .status { width: fit-content; height: fit-content; - padding: 15px 47px; + padding: 12px 35px; border-radius: 15px; color: #FFFFFF; white-space: nowrap; @@ -65,30 +68,33 @@ .completed { background-color: #B0B0B0; + font-size: 18px; } .active { background-color: #65B953; + font-size: 18px; } .surveyData { - margin-bottom: 33px; + margin-top: -15px; + margin-bottom: 15px; } .title { - font-size: 40px; + font-size: 25px; font-weight: 600; word-break: break-word; } .description { - font-size: 24px; + font-size: 17px; font-weight: 500; word-break: break-word; } .date { - font-size: 18px; + font-size: 15px; font-weight: 500; color: #7D7983; } diff --git a/SurveyFrontend/src/components/Navigation/Navigation.module.css b/SurveyFrontend/src/components/Navigation/Navigation.module.css index e9a8442..0772803 100644 --- a/SurveyFrontend/src/components/Navigation/Navigation.module.css +++ b/SurveyFrontend/src/components/Navigation/Navigation.module.css @@ -6,12 +6,12 @@ } .nav{ - margin: 34px 0 48px 40px; + margin: 34px 0 0 60px; background-color: white; border-radius: 20px; } .navList{ list-style: none; - padding: 52px 57px 70px 36px; + padding: 32px 37px 40px 26px; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/NavigationItem/NavigationItem.module.css b/SurveyFrontend/src/components/NavigationItem/NavigationItem.module.css index 0579eda..60a4992 100644 --- a/SurveyFrontend/src/components/NavigationItem/NavigationItem.module.css +++ b/SurveyFrontend/src/components/NavigationItem/NavigationItem.module.css @@ -2,13 +2,14 @@ .navItem{ padding: 0; - margin-bottom: 42px; + margin-bottom: 35px; } .page{ background-color: white; border: none; - font-size: 24px; + outline: none; + font-size: 18px; font-weight: 600; color: #AFAFAF; } diff --git a/SurveyFrontend/src/components/QuestionItem/QuestionItem.module.css b/SurveyFrontend/src/components/QuestionItem/QuestionItem.module.css index 8d7bf34..d89deff 100644 --- a/SurveyFrontend/src/components/QuestionItem/QuestionItem.module.css +++ b/SurveyFrontend/src/components/QuestionItem/QuestionItem.module.css @@ -4,7 +4,7 @@ background-color: white; display: flex; justify-content: space-between; - margin-bottom: 34px; + margin-bottom: 20px; padding: 27px 29px 26px 36px; border-radius: 14px; } @@ -22,15 +22,17 @@ } .questionTextarea{ + font-family: Monserrat, sans-serif; width: 70%; align-items: center; border: none; outline: none; resize: none; - margin-bottom: 5px; - font-size: 24px; + margin-bottom: 24px; + margin-left: -2px; + font-size: 18px; + margin-top: -2px; font-weight: 600; - line-height: 1.5; overflow-y: hidden; min-height: 1em; } @@ -44,11 +46,12 @@ } .textQuestion{ + min-height: 1em; margin-top: 0; width: 100%; - font-size: 24px; + font-size: 18px; font-weight: 600; - margin-bottom: 35px; + margin-bottom: 30px; text-align: start; word-break: break-word; } @@ -59,7 +62,7 @@ } .deleteQuestionButton{ - font-size: 18px; + font-size: 15px; font-weight: 500; color: #EC221F; border: none; @@ -72,6 +75,6 @@ .basketImg{ vertical-align: middle; - width: 24px; + width: 20px; color: #EC221F; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx b/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx index 18627bf..f07ee42 100644 --- a/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx +++ b/SurveyFrontend/src/components/QuestionItem/QuestionItem.tsx @@ -9,9 +9,9 @@ import { deleteAnswerVariant, getAnswerVariants, updateAnswerVariant -} from "../../api/AnswerApi.ts"; -import {useLocation} from "react-router-dom"; +} from "../../api/AnswerVariantsApi.ts"; import TextareaAutosize from "react-textarea-autosize"; +import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts"; interface QuestionItemProps { questionId: number; @@ -23,8 +23,8 @@ interface QuestionItemProps { onDeleteQuestion: (index: number) => Promise; initialQuestionType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion'; onQuestionTypeChange: (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => void; - surveyId?: number; + onAnswerSelect?: (questionId: number, answerText: string) => void; } const QuestionItem: React.FC = ({ @@ -37,7 +37,8 @@ const QuestionItem: React.FC = ({ onDeleteQuestion, initialQuestionType, onQuestionTypeChange, - surveyId + surveyId, + onAnswerSelect }) => { const [textQuestion, setTextQuestion] = useState(initialTextQuestion); const [isEditingQuestion, setIsEditingQuestion] = useState(false); @@ -45,8 +46,7 @@ const QuestionItem: React.FC = ({ const [questionType, setQuestionType] = useState<'SingleAnswerQuestion' | 'MultipleAnswerQuestion'>(initialQuestionType); const textareaQuestionRef = useRef(null); - const location = useLocation(); - const isCompleteSurveyActive = location.pathname === '/complete-survey'; + const isReadOnly = useRouteReadOnly(); useEffect(() => { @@ -63,6 +63,8 @@ const QuestionItem: React.FC = ({ }; const handleAddAnswer = async () => { + if (isReadOnly) return + if (!surveyId) { onAnswerVariantsChange([...initialAnswerVariants, { text: '' }]); return; @@ -123,8 +125,6 @@ const QuestionItem: React.FC = ({ if (surveyId && newAnswerVariants[index].id) { try { await updateAnswerVariant( - surveyId, - questionId, newAnswerVariants[index].id!, { text: value } ); @@ -139,7 +139,7 @@ const QuestionItem: React.FC = ({ if (surveyId && answerToDelete.id) { try { - await deleteAnswerVariant(surveyId, questionId, answerToDelete.id); + await deleteAnswerVariant(answerToDelete.id); const newAnswerVariants = initialAnswerVariants.filter((_, i) => i !== index); onAnswerVariantsChange(newAnswerVariants); setSelectedAnswers(selectedAnswers.filter((i) => i !== index)); @@ -167,12 +167,28 @@ const QuestionItem: React.FC = ({ } }; + // const toggleSelect = (index: number) => { + // if (initialQuestionType === 'SingleAnswerQuestion') { + // setSelectedAnswers([index]); + // } else { + // setSelectedAnswers(prev => + // prev.includes(index) + // ? prev.filter(i => i !== index) + // : [...prev, index] + // ); + // } + // }; + const toggleSelect = (index: number) => { + const answerText = initialAnswerVariants[index].text; + + if (onAnswerSelect) { + onAnswerSelect(questionId, answerText); + } + if (initialQuestionType === 'SingleAnswerQuestion') { - // Для одиночного выбора: заменяем массив одним выбранным индексом setSelectedAnswers([index]); } else { - // Для множественного выбора: добавляем/удаляем индекс setSelectedAnswers(prev => prev.includes(index) ? prev.filter(i => i !== index) @@ -183,7 +199,7 @@ const QuestionItem: React.FC = ({ return (
- {isCompleteSurveyActive ? ( + {isReadOnly ? (

{textQuestion || initialTextQuestion}

@@ -196,7 +212,6 @@ const QuestionItem: React.FC = ({ value={answer.text} isSelected={selectedAnswers.includes(index)} toggleSelect={() => toggleSelect(index)} - isCompleteSurveyActive={isCompleteSurveyActive} /> ))}
diff --git a/SurveyFrontend/src/components/QuestionsList/QuestionsList.module.css b/SurveyFrontend/src/components/QuestionsList/QuestionsList.module.css index c75e0d5..e69de29 100644 --- a/SurveyFrontend/src/components/QuestionsList/QuestionsList.module.css +++ b/SurveyFrontend/src/components/QuestionsList/QuestionsList.module.css @@ -1,16 +0,0 @@ -/*QuestionsList.module.css*/ - -.departur_button{ - display: block; - margin: 10px auto; - padding: 25px 50.5px; - border: none; - border-radius: 20px; - background-color: #3788D6; - color: white; - font-weight: 700; - font-size: 24px; - text-align: center; - box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1); - box-sizing: border-box; -} \ No newline at end of file diff --git a/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx b/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx index d755eb7..730a83a 100644 --- a/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx +++ b/SurveyFrontend/src/components/QuestionsList/QuestionsList.tsx @@ -2,14 +2,14 @@ import React from "react"; import QuestionItem from "../QuestionItem/QuestionItem.tsx"; import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx"; import {addNewQuestion, deleteQuestion, getListQuestions} from "../../api/QuestionApi.ts"; -import {addNewAnswerVariant} from "../../api/AnswerApi.ts"; -import {useLocation} from "react-router-dom"; -import styles from './QuestionsList.module.css' +import {addNewAnswerVariant} from "../../api/AnswerVariantsApi.ts"; +import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts"; interface QuestionsListProps { questions: Question[]; setQuestions: (questions: Question[]) => void; surveyId?: number; + onAnswerSelect?: (questionId: number, answerText: string) => void; } export interface Question { @@ -22,9 +22,8 @@ export interface Question { }>; } -const QuestionsList: React.FC = ({questions, setQuestions, surveyId}) => { - const location = useLocation(); - const isCompleteSurveyActive = location.pathname === '/complete-survey'; +const QuestionsList: React.FC = ({questions, setQuestions, surveyId, onAnswerSelect}) => { + const isReadOnly = useRouteReadOnly(); const handleAddQuestion = async () => { if (!surveyId) { @@ -72,7 +71,7 @@ const QuestionsList: React.FC = ({questions, setQuestions, s if (surveyId) { const listQuestions = await getListQuestions(surveyId); if (listQuestions.find(q => q.id === id)) { - const response = await deleteQuestion(surveyId, id); + const response = await deleteQuestion(id); if (!response?.success) { throw new Error('Не удалось удалить вопрос на сервере'); } @@ -126,11 +125,10 @@ const QuestionsList: React.FC = ({questions, setQuestions, s initialQuestionType={question.questionType} onQuestionTypeChange={(type) => handleQuestionTypeChange(question.id, type)} surveyId={surveyId} + onAnswerSelect={onAnswerSelect} /> ))} - {!isCompleteSurveyActive ? : ( - - )} + {!isReadOnly ? : ''} ); diff --git a/SurveyFrontend/src/components/RegisterForm/RegisterForm.module.css b/SurveyFrontend/src/components/RegisterForm/RegisterForm.module.css index cac537d..9564f28 100644 --- a/SurveyFrontend/src/components/RegisterForm/RegisterForm.module.css +++ b/SurveyFrontend/src/components/RegisterForm/RegisterForm.module.css @@ -1,18 +1,19 @@ .registerContainer{ - width: 31%; + width: 26%; + height: fit-content; background-color: #FFFFFF; - padding: 94px 80px; - margin: auto; + padding: 60px 50px; + margin: 0 auto; border-radius: 43px; } .title{ text-align: center; font-weight: 600; - font-size: 40px; + font-size: 30px; line-height: 88%; padding: 0; - margin-bottom: 80px; + margin-bottom: 60px; margin-top: 0; } @@ -20,12 +21,12 @@ text-align: center; display: flex; flex-direction: column; - gap: 80px; - margin-bottom: 80px; + gap: 50px; + margin-bottom: 40px; } .input { - font-size: 24px; + font-size: 18px; font-weight: 600; line-height: 88%; color: #000000; @@ -37,7 +38,7 @@ } .input::placeholder { - font-size: 24px; + font-size: 18px; font-weight: 600; line-height: 88%; color: #000000; @@ -59,12 +60,12 @@ .signUp{ margin: auto; - padding: 25.5px 16px; + padding: 22.5px 14px; width: fit-content; border-radius: 24px; background-color: #3788D6; color: #FFFFFF; - font-size: 24px; + font-size: 20px; font-weight: 600; line-height: 120%; border: none; @@ -73,8 +74,9 @@ .recommendation{ text-align: center; - font-size: 18px; + font-size: 14px; font-weight: 500; + margin-bottom: 0; } .recommendationLink{ @@ -94,7 +96,7 @@ .errorMessage{ text-align: left; - font-size: 14px; + font-size: 12px; font-weight: 400; line-height: 88%; color: #C0231F; diff --git a/SurveyFrontend/src/components/RegisterForm/RegisterForm.tsx b/SurveyFrontend/src/components/RegisterForm/RegisterForm.tsx index f7163fc..45507b0 100644 --- a/SurveyFrontend/src/components/RegisterForm/RegisterForm.tsx +++ b/SurveyFrontend/src/components/RegisterForm/RegisterForm.tsx @@ -38,11 +38,8 @@ const RegisterForm = () => { const responseData = await registerUser({username, firstName, lastName, email, password}); if (responseData && !responseData.error) { console.log('Регистрация успешна'); - localStorage.setItem("user", JSON.stringify({ - firstName, - lastName - })); - navigate('/my-surveys'); + localStorage.setItem("user", JSON.stringify(responseData.user)); + navigate('/my-surveys', {replace: true}); } else if (responseData.status === 409){ setError('Аккаунт с такой почтой уже зарегистрирован'); diff --git a/SurveyFrontend/src/components/Results/Results.module.css b/SurveyFrontend/src/components/Results/Results.module.css index e54d5f2..6de347c 100644 --- a/SurveyFrontend/src/components/Results/Results.module.css +++ b/SurveyFrontend/src/components/Results/Results.module.css @@ -9,7 +9,7 @@ display: flex; justify-content: space-between; align-items: stretch; - margin: 30px 0; + margin: 20px 0; gap: 17px; } @@ -17,7 +17,7 @@ display: flex; flex-direction: column; border-radius: 15px; - min-height: 180px; + min-height: 140px; padding: 20px; box-sizing: border-box; position: relative; @@ -43,9 +43,9 @@ } .statItem h3 { - margin: 0 0 15px 0; + margin: 0 0 0 0; color: #FFFFFF; - font-size: 28px; + font-size: 20px; font-weight: 600; line-height: 1.2; } @@ -68,20 +68,20 @@ .countAnswer p, .completion_percentage p { - font-size: 60px; + margin-bottom: -10px; + font-size: 35px; } .imgGroup, .imgSend { - width: 58px; - height: 61px; + width: 43px; align-self: flex-end; } .status p { text-align: center; margin-top: auto; - font-size: 32px; + font-size: 26px; } .questionContainer { @@ -106,13 +106,13 @@ .textContainer { display: flex; flex-direction: column; - gap: 11px; + gap: 5px; width: 30%; min-width: 250px; } .questionContainer h3 { - font-size: 24px; + font-size: 20px; font-weight: 600; color: #000000; margin: 0; @@ -120,7 +120,7 @@ .answerCount { color: #000000; - font-size: 18px; + font-size: 16px; font-weight: 600; } @@ -133,16 +133,31 @@ } .pieContainer { - width: 100%; - height: 450px; + width: 70%; position: relative; } .barContainer { width: 100%; - height: 450px; display: flex; justify-content: center; align-items: center; padding-right: 150px; } + +.exportButtonContainer { + padding: 10px 15px; + /*background-color: #4CAF50;*/ + background-color: #3788D6; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + display: block; + margin: 30px auto 15px; +} + +.exportButtonContainer:hover { + background-color: #45a049; +} \ No newline at end of file diff --git a/SurveyFrontend/src/components/Results/Results.tsx b/SurveyFrontend/src/components/Results/Results.tsx index 24d5f1f..4a02574 100644 --- a/SurveyFrontend/src/components/Results/Results.tsx +++ b/SurveyFrontend/src/components/Results/Results.tsx @@ -3,66 +3,170 @@ import styles from './Results.module.css'; import {Bar, Pie} from 'react-chartjs-2'; import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js'; import {useOutletContext} from "react-router-dom"; -import {ISurvey} from "../../api/SurveyApi.ts"; +import {ISurvey, getSurveyById} from "../../api/SurveyApi.ts"; import ChartDataLabels from 'chartjs-plugin-datalabels'; import annotationPlugin from 'chartjs-plugin-annotation'; import Group from '../../assets/gmail_groups.svg?react'; import Send from '../../assets/send.svg?react'; - +import {getAllCompletions} from "../../api/CompletionApi.ts"; +import {getAnswer} from "../../api/AnswerApi.ts"; +import {useEffect, useState} from "react"; +import {getListQuestions} from "../../api/QuestionApi.ts"; +import {getAnswerVariants, IAnswerVariant} from "../../api/AnswerVariantsApi.ts"; +import {getResultsFile} from "../../api/ExportResultApi.ts"; ChartJS.register( ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title, ChartDataLabels, annotationPlugin ); -// Типы для данных interface QuestionStats { questionText: string; totalAnswers: number; options: { text: string; percentage: number; + id: number; }[]; - isMultipleChoice?: boolean; + isMultipleChoice: boolean; +} + +interface IAnswer { + questionId: number; + completionId: number; + answerText: string; + optionId?: number; } export const Results = () => { - const { survey, setSurvey } = useOutletContext<{ + const { survey: initialSurvey, setSurvey } = useOutletContext<{ survey: ISurvey; setSurvey: (survey: ISurvey) => void; }>(); - - const surveyStats = { - totalParticipants: 100, - completionPercentage: 80, + const [surveyStats, setSurveyStats] = useState({ + totalParticipants: 0, + completionPercentage: 0, status: 'Активен', - questions: [ - { - questionText: "Вопрос 1", - totalAnswers: 80, - options: [ - { text: "Вариант 1", percentage: 46 }, - { text: "Вариант 2", percentage: 15 }, - { text: "Вариант 3", percentage: 39 } - ], - isMultipleChoice: false - }, - { - questionText: "Вопрос 2", - totalAnswers: 100, - options: [ - { text: "Вариант 1", percentage: 50 }, - { text: "Вариант 2", percentage: 20 }, - { text: "Вариант 3", percentage: 100 }, - { text: "Вариант 4", percentage: 80 } - ], - isMultipleChoice: true - } - ] as QuestionStats[] + questions: [] as QuestionStats[] + }); + + const [survey, setLocalSurvey] = useState(initialSurvey); + // const [questions, setQuestions] = useState([]); + + const handleExportToExcel = async (id: number) => { + await getResultsFile(id) }; - // Цветовая палитра + useEffect(() => { + const fetchSurveyData = async () => { + try { + const surveyData = await getSurveyById(survey.id); + setLocalSurvey(surveyData); + + const questionsList = await getListQuestions(survey.id); + // setQuestions(questionsList); + + const questionsWithVariants = await Promise.all( + questionsList.map(async (question) => { + const variants = await getAnswerVariants(survey.id, question.id); + return { + ...question, + options: variants, + isMultipleChoice: question.questionType !== 'SingleAnswerQuestion' + }; + }) + ); + + const completions = await getAllCompletions(survey.id); + const totalParticipants = completions.length; + + const questionsWithAnswers = await Promise.all( + questionsWithVariants.map(async (question) => { + const answers = await getAnswer(question.id); + return { + ...question, + answers: answers + }; + }) + ); + + const questionsStats = questionsWithAnswers.map(question => { + const questionAnswers = question.answers as IAnswer[]; + + let uniqueAnswers = questionAnswers; + if (question.isMultipleChoice) { + const groupedByCompletion = questionAnswers.reduce((acc, answer) => { + if (!acc[answer.completionId]) { + acc[answer.completionId] = []; + } + acc[answer.completionId].push(answer); + return acc; + }, {} as Record); + + uniqueAnswers = Object.values(groupedByCompletion).map(group => ({ + ...group[0], + answerText: group.map(a => a.answerText).join("; ") + })); + } + + const optionsStats = question.options.map((option: IAnswerVariant) => { + const count = uniqueAnswers.filter(answer => + question.isMultipleChoice + ? answer.answerText.includes(option.text) + : answer.answerText === option.text + ).length; + + const percentage = totalParticipants > 0 + ? Math.round((count / totalParticipants) * 100) + : 0; + + return { + text: option.text, + percentage: percentage, + id: option.id + }; + }); + + return { + questionText: question.title, + totalAnswers: uniqueAnswers.length, + options: optionsStats, + isMultipleChoice: question.isMultipleChoice + }; + }); + + const totalAnswersCount = questionsWithAnswers.reduce((sum, question) => { + if (question.isMultipleChoice) { + const uniqueCompletions = new Set( + question.answers.map((answer : IAnswer) => answer.completionId) + ); + return sum + uniqueCompletions.size; + } else { + return sum + question.answers.length; + } + }, 0); + + const maxPossibleAnswers = questionsWithAnswers.length * totalParticipants; + const completionPercentage = totalParticipants > 0 && maxPossibleAnswers > 0 + ? Math.round((totalAnswersCount / maxPossibleAnswers) * 100) + : 0; + + setSurveyStats({ + totalParticipants, + completionPercentage, + status: 'Активен', + questions: questionsStats + }); + + } catch (error) { + console.error('Error fetching survey data:', error); + } + }; + + fetchSurveyData(); + }, [survey.id]); + const colorsForPie = ['#67C587', '#C9EAD4', '#EAF6ED']; const colorsForBar = ['#8979FF']; @@ -71,8 +175,14 @@ export const Results = () => { setSurvey({ ...survey, description: value })} - setTitleSurvey={(value) => setSurvey({ ...survey, title: value })} + setDescriptionSurvey={(value) => { + setSurvey({ ...survey, description: value }); + setLocalSurvey({ ...survey, description: value }); + }} + setTitleSurvey={(value) => { + setSurvey({ ...survey, title: value }); + setLocalSurvey({ ...survey, title: value }); + }} />
@@ -133,7 +243,7 @@ export const Results = () => { xValue: i, yValue: opt.percentage + 5, content: `${opt.percentage}%`, - font: { size: 16, weight: 400 }, + font: { size: 1, weight: 400 }, color: '#000' })) } @@ -148,7 +258,7 @@ export const Results = () => { ticks: { color: '#000000', font: { - size: 16, + size: 12, weight: 400 } }, @@ -178,7 +288,7 @@ export const Results = () => { labels: { color: '#000000', font: { - size: 18, + size: 12, weight: 500 } } @@ -191,7 +301,7 @@ export const Results = () => { datalabels: { formatter: (value) => `${value}%`, color: '#000', - font: { weight: 400, size: 16 } + font: { weight: 400, size: 12 } } }, animation: { animateRotate: true } @@ -203,6 +313,8 @@ export const Results = () => {
))} + +
); }; \ No newline at end of file diff --git a/SurveyFrontend/src/components/SaveButton/SaveButton.module.css b/SurveyFrontend/src/components/SaveButton/SaveButton.module.css index 54d2c03..da79f24 100644 --- a/SurveyFrontend/src/components/SaveButton/SaveButton.module.css +++ b/SurveyFrontend/src/components/SaveButton/SaveButton.module.css @@ -2,14 +2,14 @@ .createSurveyButton { display: block; - margin: 10px auto; - padding: 25px 50.5px; + margin: 0 auto; + padding: 20px 35px; border: none; border-radius: 20px; background-color: #3788D6; color: white; font-weight: 700; - font-size: 24px; + font-size: 18px; text-align: center; box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1); box-sizing: border-box; diff --git a/SurveyFrontend/src/components/SaveButton/SaveButton.tsx b/SurveyFrontend/src/components/SaveButton/SaveButton.tsx index 2f8481a..dc33adb 100644 --- a/SurveyFrontend/src/components/SaveButton/SaveButton.tsx +++ b/SurveyFrontend/src/components/SaveButton/SaveButton.tsx @@ -7,7 +7,7 @@ interface CreateSurveyButtonProps { const SaveButton: React.FC = ({onClick}) => { return ( - ); diff --git a/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.module.css b/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.module.css index 444399f..8fd3d2d 100644 --- a/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.module.css +++ b/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.module.css @@ -14,14 +14,31 @@ .param{ border-radius: 4px; background-color: #FFFFFF; - padding-top: 15px; - padding-bottom: 97px; + padding-top: 8px; + padding-bottom: 60px; padding-left: 19px; margin-bottom: 30px; } .param h2{ - font-size: 24px; + font-size: 18px; font-weight: 600; border-radius: 4px; +} + +.copyButton { + padding: 10px 15px; + /*background-color: #4CAF50;*/ + background-color: #3788D6; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + display: block; + margin: 30px auto 15px; +} + +.copyButton:hover { + background-color: #45a049; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx b/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx index a2a6568..ad2d5f7 100644 --- a/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx +++ b/SurveyFrontend/src/components/SettingSurvey/SettingSurvey.tsx @@ -1,10 +1,11 @@ -import React, {useState} from 'react'; +import React from 'react'; import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; import styles from "./SettingSurvey.module.css"; import TimeEvent from "../TimeEvent/TimeEvent.tsx"; import SaveButton from "../SaveButton/SaveButton.tsx"; import {ISurvey} from "../../api/SurveyApi.ts"; import {useLocation, useOutletContext} from "react-router-dom"; +import {useSurveyContext} from "../../context/SurveyContext.tsx"; const SettingSurvey: React.FC = () => { @@ -14,18 +15,25 @@ const SettingSurvey: React.FC = () => { survey: ISurvey; setSurvey: (survey: ISurvey) => void; }>(); + const { tempSurvey, setTempSurvey } = useSurveyContext(); - const [descriptionSurvey, setDescriptionSurvey] = useState(''); - const [titleSurvey, setTitleSurvey] = useState(''); + const handleCopyLink = () => { + if (!survey?.id) + return; + const link = `${window.location.origin}/complete-survey/${survey.id}`; + navigator.clipboard.writeText(link) + .then(() => console.log('Copied!')) + .catch(error => console.error(`Не удалось скопировать ссылку: ${error}`)); + } return (
{isSettingCreatePage ? ( setTempSurvey({ ...tempSurvey, description: value })} + setTitleSurvey={(value) => setTempSurvey({ ...tempSurvey, title: value })} /> ) : ( {

Параметры видимости

{}}/> + {!isSettingCreatePage ? : ''}
) } diff --git a/SurveyFrontend/src/components/Survey/Survey.tsx b/SurveyFrontend/src/components/Survey/Survey.tsx index 802e0d4..2e0c676 100644 --- a/SurveyFrontend/src/components/Survey/Survey.tsx +++ b/SurveyFrontend/src/components/Survey/Survey.tsx @@ -1,64 +1,78 @@ -import React, {useState} from "react"; +import React, { useState, useEffect } from "react"; import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; -import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx"; -import styles from './Survey.module.css' +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"; -import {addNewAnswerVariant} from "../../api/AnswerApi.ts"; +import { ISurvey, postNewSurvey } from "../../api/SurveyApi.ts"; +import { addNewQuestion } from "../../api/QuestionApi.ts"; +import { useLocation, useNavigate } from "react-router-dom"; +import { addNewAnswerVariant } from "../../api/AnswerVariantsApi.ts"; +import { useSurveyContext } from "../../context/SurveyContext.tsx"; const Survey: React.FC = () => { const navigate = useNavigate(); - const [descriptionSurvey, setDescriptionSurvey] = useState(''); - const [titleSurvey, setTitleSurvey] = useState('Название опроса'); + const location = useLocation(); + const { tempSurvey, setTempSurvey, clearTempSurvey } = useSurveyContext(); const [survey] = useState(null); - const [questions, setQuestions] = useState([ - { id: 1, text: '', questionType: 'SingleAnswerQuestion', answerVariants: [{ text: '' }]}, - ]); + const [questions, setQuestions] = useState( + tempSurvey.questions.length > 0 + ? tempSurvey.questions + : [{ id: 1, text: '', questionType: 'SingleAnswerQuestion', answerVariants: [{ text: '' }] }] + ); + + useEffect(() => { + if (!tempSurvey.title && !tempSurvey.description && tempSurvey.questions.length === 0) { + setTempSurvey({ + title: "Название опроса", + description: "", + questions: [{ id: 1, text: '', questionType: 'SingleAnswerQuestion', answerVariants: [{ text: '' }] }] + }); + setQuestions([{ id: 1, text: '', questionType: 'SingleAnswerQuestion', answerVariants: [{ text: '' }] }]); + } + }, []); + + useEffect(() => { + setTempSurvey({ + ...tempSurvey, + questions: questions, + }); + }, [questions]); + + useEffect(() => { + const isCreateSurveyRoute = location.pathname.includes('/survey/create'); + + if (!isCreateSurveyRoute) { + clearTempSurvey(); + setQuestions([{ id: 1, text: '', questionType: 'SingleAnswerQuestion', answerVariants: [{ text: '' }] }]); + } + }, [location.pathname]); const handleSave = async () => { try { const savedSurvey = await postNewSurvey({ - title: titleSurvey, - description: descriptionSurvey + title: tempSurvey.title, + description: tempSurvey.description, }); - const updatedQuestions: Question[] = []; - for (const question of questions) { + const questionPromises = questions.map(async (question) => { const newQuestion = await addNewQuestion(savedSurvey.id, { title: question.text, - questionType: question.questionType + questionType: question.questionType, }); - const updatedQuestion: Question = { - ...question, - id: newQuestion.id, - answerVariants: [] - }; - - if (question.answerVariants && question.answerVariants.length > 0) { - const newVariants = await Promise.all( + if (question.answerVariants?.length > 0) { + await Promise.all( question.answerVariants.map(answer => - addNewAnswerVariant( - savedSurvey.id, - newQuestion.id, - { text: answer.text } - ) + addNewAnswerVariant(savedSurvey.id, newQuestion.id, { text: answer.text }) ) ); - - updatedQuestion.answerVariants = newVariants.map((variant: { id: number, text: string }) => ({ - id: variant.id, - text: variant.text - })); } + }); - updatedQuestions.push(updatedQuestion); - } + await Promise.all(questionPromises); - setQuestions(updatedQuestions); + clearTempSurvey(); navigate('/my-surveys'); } catch (error) { console.error('Ошибка при сохранении:', error); @@ -68,20 +82,19 @@ const Survey: React.FC = () => { return (
setTempSurvey({ ...tempSurvey, description: value })} + setTitleSurvey={(value) => setTempSurvey({ ...tempSurvey, title: value })} /> - - +
); -} +}; export default Survey; \ No newline at end of file diff --git a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css index 3ba7298..6f229cd 100644 --- a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css +++ b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.module.css @@ -5,33 +5,30 @@ padding: 0; width: 100%; margin-top: 34px; - margin-bottom: 49px; + margin-bottom: 30px; border-radius: 14px; - /*min-height: 191px;*/ - /*max-height: 100vh;*/ max-height: fit-content; } .info{ - min-width: 373px; - /*display: block;*/ - padding: 35px; - display: flex; /* Добавляем flex */ - flex-direction: column; /* Элементы в колонку */ + min-width: 300px; + padding: 20px 35px; + display: flex; + flex-direction: column; align-items: center; } .titleSurvey{ + min-height: 60px; width: 80%; display: block; border: none; + outline: none; margin: 0 auto 13px; background-color: white; text-align: center; - font-size: 20px; + font-size: 26px; font-weight: 600; - /*margin-bottom: 23px;*/ - /*margin-bottom: 15px;*/ word-break: break-word; padding: 0; } @@ -52,7 +49,9 @@ } .textareaTitle { - font-size: 32px; + margin-top: 14px; + margin-bottom: -1px; + font-size: 26px; font-weight: 600; text-align: center; line-height: 1.2; @@ -60,11 +59,13 @@ } .textareaDescrip { - font-size: 18px; + font-size: 16px; font-weight: 500; text-align: center; line-height: 1.4; min-height: 24px; + margin-top: -2px; + margin-bottom: -3px; } .descriptionWrapper { @@ -74,8 +75,10 @@ } .description { + min-height: 24px; border: none; - font-size: 24px; + outline: none; + font-size: 16px; font-weight: 500; text-align: center; background-color: white; @@ -87,7 +90,8 @@ } .desc{ - font-size: 24px; + font-size: 20px; + outline: none; font-weight: 500; background-color: white; max-width: 80%; @@ -105,12 +109,12 @@ .descButtonImg{ vertical-align: middle; - width: 28px; + width: 20px; } .textButton{ vertical-align: middle; - font-size: 24px; + font-size: 16px; font-weight: 500; color: #7D7983; padding: 10px; @@ -118,7 +122,7 @@ .createdAt{ text-align: center; - font-size: 18px; + font-size: 15px; font-weight: 500; color: #7D7983; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx index 3ce15ff..cf35990 100644 --- a/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx +++ b/SurveyFrontend/src/components/SurveyInfo/SurveyInfo.tsx @@ -3,6 +3,7 @@ import styles from './SurveyInfo.module.css' import AddDescripImg from '../../assets/add_circle.svg?react'; import TextareaAutosize from 'react-textarea-autosize'; import {useLocation} from "react-router-dom"; +import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts"; interface SurveyInfoProps { @@ -20,10 +21,11 @@ const SurveyInfo: React.FC = ({titleSurvey, setDescriptionSurve const descriptionTextareaRef = useRef(null); const location = useLocation(); - const isCompleteSurveyActive = location.pathname === '/complete-survey'; const isSurveyViewPage = location.pathname.startsWith('/survey/') && !location.pathname.startsWith('/survey/create'); + const isReadOnly = useRouteReadOnly(); + const handleDescriptionChange = (descripEvent: React.ChangeEvent) => { setDescriptionSurvey?.(descripEvent.target.value); }; @@ -88,17 +90,15 @@ const SurveyInfo: React.FC = ({titleSurvey, setDescriptionSurve } const renderTitle = () => { - if (isCompleteSurveyActive) { + if (isReadOnly) { return ( - + ) } if (showNewTitleField) { return ( -

+ //

= ({titleSurvey, setDescriptionSurve onKeyDown={handleTitleKeyDown} onBlur={handleTitleBlur} /> -

+ // ); } return ( - + ); }; const renderDescription = () => { - if (isCompleteSurveyActive) { + if (isReadOnly) { return descriptionSurvey ? (

{descriptionSurvey}

- ) : 'Описание'; + ) : ''; } if (descriptionSurvey && !showDescriptionField) { @@ -167,7 +165,7 @@ const SurveyInfo: React.FC = ({titleSurvey, setDescriptionSurve {renderTitle()} {renderDescription()} - {(isSurveyViewPage || isCompleteSurveyActive) && createdAt && ( + {(isSurveyViewPage || isReadOnly) && createdAt && (

Дата создания: {addDate()}

)} diff --git a/SurveyFrontend/src/components/SurveyPage/SurveyPage.module.css b/SurveyFrontend/src/components/SurveyPage/SurveyPage.module.css index ebe030f..bc33a4f 100644 --- a/SurveyFrontend/src/components/SurveyPage/SurveyPage.module.css +++ b/SurveyFrontend/src/components/SurveyPage/SurveyPage.module.css @@ -6,5 +6,5 @@ color: #C0231F; text-align: center; margin: 10px 0; - font-size: 18px; + font-size: 15px; } \ No newline at end of file diff --git a/SurveyFrontend/src/components/SurveyPage/SurveyPage.tsx b/SurveyFrontend/src/components/SurveyPage/SurveyPage.tsx index 16c2db9..6b5dfbc 100644 --- a/SurveyFrontend/src/components/SurveyPage/SurveyPage.tsx +++ b/SurveyFrontend/src/components/SurveyPage/SurveyPage.tsx @@ -6,7 +6,7 @@ import {useOutletContext} from "react-router-dom"; import { addNewQuestion, getListQuestions, updateQuestion, deleteQuestion } from "../../api/QuestionApi.ts"; import styles from "./SurveyPage.module.css"; import SaveButton from "../SaveButton/SaveButton.tsx"; -import { addNewAnswerVariant, deleteAnswerVariant, getAnswerVariants, IAnswerVariant, updateAnswerVariant } from "../../api/AnswerApi.ts"; +import { addNewAnswerVariant, deleteAnswerVariant, getAnswerVariants, IAnswerVariant, updateAnswerVariant } from "../../api/AnswerVariantsApi.ts"; type ActionType = | 'update-survey' @@ -118,14 +118,14 @@ class ActionQueue { } private async handleUpdateQuestion(data: QuestionActionData & { id: number }) { - return await updateQuestion(data.surveyId, data.id, { + return await updateQuestion(data.id, { title: data.title, questionType: data.questionType }); } private async handleDeleteQuestion(data: QuestionActionData & { id: number }) { - return await deleteQuestion(data.surveyId, data.id); + return await deleteQuestion(data.id); } private async handleCreateAnswer(data: AnswerActionData) { @@ -136,7 +136,7 @@ class ActionQueue { private async handleUpdateAnswer(data: AnswerActionData & { id: number }) { try { - const result = await updateAnswerVariant(data.surveyId, data.questionId, data.id, { + const result = await updateAnswerVariant(data.id, { text: data.text }); return result; @@ -148,16 +148,14 @@ class ActionQueue { } private async handleDeleteAnswer(data: AnswerActionData & { id: number }) { - return await deleteAnswerVariant(data.surveyId, data.questionId, data.id); + return await deleteAnswerVariant(data.id); } } export const SurveyPage: React.FC = () => { - // const [survey, setSurvey] = useState(null); const [questions, setQuestions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // const { surveyId } = useParams<{ surveyId: string }>(); const [description, setDescription] = useState(''); const [title, setTitle] = useState(''); @@ -173,7 +171,6 @@ export const SurveyPage: React.FC = () => { return; } - // const id = parseInt(survey.id); const id = survey.id; if (isNaN(id)) { console.error('Invalid survey ID'); @@ -183,7 +180,6 @@ export const SurveyPage: React.FC = () => { const fetchData = async () => { try { setLoading(true); - // const surveyData = await getSurveyById(id); setSurvey(survey); setTitle(survey.title); setDescription(survey.description); @@ -218,7 +214,6 @@ export const SurveyPage: React.FC = () => { try { setError(null); - // const id = parseInt(survey.id); const id = survey.id; const actionQueue = new ActionQueue(); diff --git a/SurveyFrontend/src/components/TimeEvent/TimeEvent.module.css b/SurveyFrontend/src/components/TimeEvent/TimeEvent.module.css index 34b45be..c5abbca 100644 --- a/SurveyFrontend/src/components/TimeEvent/TimeEvent.module.css +++ b/SurveyFrontend/src/components/TimeEvent/TimeEvent.module.css @@ -3,7 +3,7 @@ .timeEvent{ width: 44%; - padding: 17px 25px 48px 20px; + padding: 8px 25px 28px 20px; background-color: #FFFFFF; border-radius: 6px; margin-bottom: 34px; @@ -11,7 +11,7 @@ .title{ font-weight: 600; - font-size: 24px; + font-size: 18px; margin-bottom: 23px; } @@ -24,8 +24,9 @@ .inputDate{ width: fit-content; border: 3px solid #007AFF26; - padding: 12px 40px 12px 21px; - font-size: 20px; + padding: 8px 25px 8px 21px; + font-family: inherit; + font-size: 15px; font-weight: 400; border-radius: 3px; } @@ -33,8 +34,8 @@ .inputTime{ width: fit-content; border: 3px solid #007AFF26; - padding: 12px 22px; - font-size: 20px; + padding: 8px 25px; + font-size: 15px; font-weight: 400; border-radius: 3px; } diff --git a/SurveyFrontend/src/components/TypeDropdown/TypeDropdown.module.css b/SurveyFrontend/src/components/TypeDropdown/TypeDropdown.module.css index ac7dc68..e7a62e6 100644 --- a/SurveyFrontend/src/components/TypeDropdown/TypeDropdown.module.css +++ b/SurveyFrontend/src/components/TypeDropdown/TypeDropdown.module.css @@ -1,6 +1,7 @@ /*TypeDropdown.module.css*/ .dropdownContainer { + margin-top: -5px; width: 23%; position: relative; display: inline-block; @@ -12,7 +13,7 @@ border: 1px solid #000000; border-radius: 19px; padding: 9px 7px 7px 10px; - font-size: 16px; + font-size: 13px; font-weight: 500; cursor: pointer; display: flex; @@ -25,7 +26,6 @@ .selectedTypeIcon { margin-right: 4px; - width: 22px; } .dropdownArrow { @@ -33,7 +33,7 @@ } .dropdownList { - width: 70%; + width: 85%; margin-top: 11px; position: absolute; background-color: #fff; @@ -58,13 +58,11 @@ .dropdownItemIcon { margin-right: 5px; - width: 24px; } .selectedTypeIcon, .dropdownItemIcon { - width: 20px; - height: 20px; + width: 17px; margin-right: 5px; vertical-align: middle; } diff --git a/SurveyFrontend/src/context/SurveyContext.tsx b/SurveyFrontend/src/context/SurveyContext.tsx new file mode 100644 index 0000000..a3f8437 --- /dev/null +++ b/SurveyFrontend/src/context/SurveyContext.tsx @@ -0,0 +1,37 @@ +import React, {createContext, useContext, useState} from "react"; +import { INewSurvey } from "../api/SurveyApi.ts"; +import { Question } from "../components/QuestionsList/QuestionsList.tsx"; + +interface SurveyContextType { + tempSurvey: INewSurvey & { questions: Question[] }; + setTempSurvey: (survey: INewSurvey & { questions: Question[] }) => void; + clearTempSurvey: () => void; +} + +const SurveyContext = createContext(undefined); + +export const SurveyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [tempSurvey, setTempSurvey] = useState({ + title: "", + description: "", + questions: [], + }); + + const clearTempSurvey = () => { + setTempSurvey({ title: "", description: "", questions: [] }); + }; + + return ( + + {children} + + ); +}; + +export const useSurveyContext = () => { + const context = useContext(SurveyContext); + if (!context) { + throw new Error("useSurveyContext must be used within a SurveyProvider"); + } + return context; +}; \ No newline at end of file diff --git a/SurveyFrontend/src/hooks/useRouteReadOnly.ts b/SurveyFrontend/src/hooks/useRouteReadOnly.ts new file mode 100644 index 0000000..417a8c3 --- /dev/null +++ b/SurveyFrontend/src/hooks/useRouteReadOnly.ts @@ -0,0 +1,6 @@ +import { useLocation } from 'react-router-dom'; + +export const useRouteReadOnly = () => { + const location = useLocation(); + return location.pathname.includes('/complete-survey/'); +}; \ No newline at end of file diff --git a/SurveyFrontend/src/pages/AuthForm/AuthForm.module.css b/SurveyFrontend/src/pages/AuthForm/AuthForm.module.css index bae0307..5612090 100644 --- a/SurveyFrontend/src/pages/AuthForm/AuthForm.module.css +++ b/SurveyFrontend/src/pages/AuthForm/AuthForm.module.css @@ -2,11 +2,14 @@ width: 100%; min-height: 100vh; background-color: #F6F6F6; - padding: 61.5px 0; + padding: 50px 0; } .pageLogin{ + min-height: 80vh; width: 100%; background-color: #F6F6F6; - padding: 157px 0; + padding: 100px 0; + display: flex; + justify-content: center; } \ No newline at end of file diff --git a/SurveyFrontend/src/pages/AuthForm/AuthForm.tsx b/SurveyFrontend/src/pages/AuthForm/AuthForm.tsx index f9c5fc9..fd4e626 100644 --- a/SurveyFrontend/src/pages/AuthForm/AuthForm.tsx +++ b/SurveyFrontend/src/pages/AuthForm/AuthForm.tsx @@ -2,6 +2,7 @@ import styles from './AuthForm.module.css'; import LoginForm from "../../components/LoginForm/LoginForm.tsx"; import RegisterForm from "../../components/RegisterForm/RegisterForm.tsx"; import {useLocation} from "react-router-dom"; +import {useEffect} from "react"; const AuthForm = () => { @@ -9,6 +10,22 @@ const AuthForm = () => { const isLoginPage = location.pathname === '/login'; const isRegisterPage = location.pathname === '/register'; + useEffect(() => { + if (isLoginPage || isRegisterPage) { + window.history.pushState(null, "", window.location.href); + + const handlePopState = () => { + window.history.pushState(null, "", window.location.href); + }; + + window.addEventListener("popstate", handlePopState); + + return () => { + window.removeEventListener("popstate", handlePopState); + }; + } + }, [isLoginPage, isRegisterPage]); + let content; if (isLoginPage) { content = ; diff --git a/SurveyFrontend/src/pages/CompleteSurvey/CompleteSurvey.tsx b/SurveyFrontend/src/pages/CompleteSurvey/CompleteSurvey.tsx index 32d9cd8..eed8f24 100644 --- a/SurveyFrontend/src/pages/CompleteSurvey/CompleteSurvey.tsx +++ b/SurveyFrontend/src/pages/CompleteSurvey/CompleteSurvey.tsx @@ -1,11 +1,9 @@ -import Header from "../../components/Header/Header.tsx"; import styles from './CompleteSurvey.module.css' import CompletingSurvey from "../../components/CompletingSurvey/CompletingSurvey.tsx"; export const CompleteSurvey = () => { return(
-
)