Merge branch 'correction' into 'unstable'

Correction

See merge request internship-2025/survey-webapp/survey-webapp!28
This commit is contained in:
Tatyana Nikolaeva 2025-06-09 11:23:58 +00:00
commit f8ee3fb80c
50 changed files with 940 additions and 472 deletions

View file

@ -9,7 +9,7 @@ import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx";
import AuthForm from "./pages/AuthForm/AuthForm.tsx"; import AuthForm from "./pages/AuthForm/AuthForm.tsx";
import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx"; import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx";
import CompleteSurvey from "./pages/CompleteSurvey/CompleteSurvey.tsx"; import CompleteSurvey from "./pages/CompleteSurvey/CompleteSurvey.tsx";
import CompletingSurvey from "./components/CompletingSurvey/CompletingSurvey.tsx"; import {SurveyProvider} from './context/SurveyContext.tsx';
const App = () => { const App = () => {
return( return(
@ -19,8 +19,8 @@ const App = () => {
<Route path="/register" element={<AuthForm />} /> <Route path="/register" element={<AuthForm />} />
<Route path="survey/create" element={<SurveyCreateAndEditingPage />}> <Route path="survey/create" element={<SurveyCreateAndEditingPage />}>
<Route path="questions" element={<Survey />} /> <Route path="questions" element={<SurveyProvider><Survey /></SurveyProvider>} />
<Route path="settings" element={<SettingSurvey />} /> <Route path="settings" element={<SurveyProvider><SettingSurvey /></SurveyProvider>} />
</Route> </Route>
<Route path="my-surveys" element={<MySurveysPage />}> <Route path="my-surveys" element={<MySurveysPage />}>
@ -29,13 +29,11 @@ const App = () => {
<Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}> <Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}>
<Route path="questions" element={<SurveyPage />} /> <Route path="questions" element={<SurveyPage />} />
<Route path="settings" element={<SettingSurvey />} /> <Route path="settings" element={<SurveyProvider><SettingSurvey /></SurveyProvider>} />
<Route path="results" element={<Results />} /> <Route path="results" element={<Results />} />
</Route> </Route>
<Route path='/complete-survey' element={<CompleteSurvey/>}> <Route path='/complete-survey/:surveyId' element={<CompleteSurvey/>}/>
<Route index element={<CompletingSurvey/>}/>
</Route>
<Route path="*" element={<AuthForm />} /> <Route path="*" element={<AuthForm />} />
</Routes> </Routes>

View file

@ -1,94 +1,40 @@
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts";
export interface INewAnswer{ export const getAnswer = async (id: number) => {
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<INewAnswer> => {
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) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (!token) { if (!token) {
throw new Error('Токен отсутствует'); throw new Error('Токен отсутствует');
} }
try{ try{
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants/${id}`, { const response = await fetch(`${BASE_URL}/questions/${id}/answers`, {
...createRequestConfig('DELETE'), ...createRequestConfig('GET'),
}) })
const responseData = await handleResponse(response); return await handleResponse(response)
if (response.ok && !responseData){
return {success: true};
} }
return responseData; catch (error) {
} handleUnauthorizedError(error);
catch(err){ console.error(`error when receiving the response: ${error}`);
console.error(`Error deleting a answer: ${err}`); throw error;
throw err; }
}
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;
} }
} }

View file

@ -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<INewAnswer> => {
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;
}
}

View file

@ -1,4 +1,4 @@
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts";
interface IAuthData{ interface IAuthData{
email: string; 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) { if (!response.ok) {
throw new Error(`Ошибка сервера: ${response.status}`); throw new Error(`Ошибка сервера: ${response.status}`);
} }
@ -42,33 +34,38 @@ export const getCurrentUser = async () => {
localStorage.setItem("user", JSON.stringify(userData)); localStorage.setItem("user", JSON.stringify(userData));
return userData; return userData;
} catch (error) { } catch (error) {
handleUnauthorizedError(error);
console.error("Ошибка при получении данных пользователя:", error); console.error("Ошибка при получении данных пользователя:", error);
throw error; throw error;
} }
}; };
export const registerUser = async (data: IRegistrationData) => { export const registerUser = async (data: IRegistrationData) => {
try{ try {
const response = await fetch(`${BASE_URL}/auth/register`, { const response = await fetch(`${BASE_URL}/auth/register`, {
...createRequestConfig('POST'), body: JSON.stringify(data), ...createRequestConfig('POST'),
}) body: JSON.stringify(data),
const responseData = await handleResponse(response); });
if (responseData.accessToken) { if (!response.ok) {
localStorage.setItem("token", responseData.accessToken); const errorData = await response.json();
localStorage.setItem("user", JSON.stringify({ throw new Error(errorData.message || `Ошибка: ${response.status}`);
firstName: data.firstName, }
lastName: data.lastName,
email: data.email 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; return responseData;
} catch (error){ } catch (error) {
console.error("Registration error:", error); console.error("Registration error:", error);
throw error; throw error;
} }
} };
export const authUser = async (data: IAuthData) => { export const authUser = async (data: IAuthData) => {
try { try {
@ -76,6 +73,7 @@ export const authUser = async (data: IAuthData) => {
...createRequestConfig('POST'), ...createRequestConfig('POST'),
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
const responseData = await handleResponse(response); const responseData = await handleResponse(response);
const token = responseData.accessToken || responseData.token; const token = responseData.accessToken || responseData.token;
@ -91,6 +89,7 @@ export const authUser = async (data: IAuthData) => {
return responseData; return responseData;
} catch (error) { } catch (error) {
handleUnauthorizedError(error);
console.error("Login error:", error); console.error("Login error:", error);
throw error; throw error;
} }

View file

@ -6,6 +6,13 @@ interface RequestConfig {
body?: BodyInit | null; body?: BodyInit | null;
} }
export const handleUnauthorizedError = (error: unknown) => {
if (error instanceof Error && error.message.includes('401')) {
window.location.href = '/login';
console.log('Сессия истекла. Перенаправление на страницу входа.');
}
};
/** /**
* Создаёт конфигурацию для fetch-запроса * Создаёт конфигурацию для fetch-запроса
* @param method HTTP-метод (GET, POST, PUT, DELETE) * @param method HTTP-метод (GET, POST, PUT, DELETE)
@ -37,14 +44,15 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque
* @returns Распарсенные данные или ошибку * @returns Распарсенные данные или ошибку
*/ */
const handleResponse = async (response: Response) => { 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(); const responseText = await response.text();
if (!responseText) { if (!responseText) {
if (response.status === 401) {
window.location.href = '/auth/login';
throw new Error('Требуется авторизация');
}
if (response.ok) { if (response.ok) {
return null; return null;
} }

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -1,5 +1,5 @@
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts";
import {IAnswerVariant} from "./AnswerApi.ts"; import {IAnswerVariant} from "./AnswerVariantsApi.ts";
export interface INewQuestion{ export interface INewQuestion{
title: string; title: string;
@ -25,6 +25,7 @@ export const addNewQuestion = async (surveyId: number, question: INewQuestion) =
}) })
return await handleResponse(response) return await handleResponse(response)
} catch (error){ } catch (error){
handleUnauthorizedError(error);
throw new Error(`Error when adding a new question: ${error}`); throw new Error(`Error when adding a new question: ${error}`);
} }
} }
@ -34,50 +35,46 @@ export const getListQuestions = async (surveyId: number): Promise<IQuestion[]> =
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions`, { const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions`, {
...createRequestConfig('GET'), ...createRequestConfig('GET'),
}) })
if (response.status === 200) {
return await handleResponse(response); return await handleResponse(response);
} }
throw new Error(`Ожидался код 200, получен ${response.status}`);
}
catch(error){ catch(error){
handleUnauthorizedError(error);
console.error(`Error when receiving the list of questions: ${error}`); console.error(`Error when receiving the list of questions: ${error}`);
throw error; throw error;
} }
} }
export const updateQuestion = async (surveyId: number, id: number, question: Partial<INewQuestion>): Promise<INewQuestion> => { export const updateQuestion = async (id: number, question: Partial<INewQuestion>): Promise<INewQuestion> => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (!token) { if (!token) {
throw new Error("Токен отсутствует"); throw new Error("Токен отсутствует");
} }
try{ try{
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${id}`, { const response = await fetch(`${BASE_URL}/questions/${id}`, {
...createRequestConfig('PUT'), ...createRequestConfig('PUT'),
body: JSON.stringify({ body: JSON.stringify({
title: question.title, title: question.title,
questionType: question.questionType, questionType: question.questionType,
}), }),
}) })
if (response.status === 200) {
return await handleResponse(response) return await handleResponse(response)
} }
throw new Error(`Ожидался код 200, получен ${response.status}`)
}
catch(error){ catch(error){
handleUnauthorizedError(error);
console.error(`Error when updating question: ${error}`); console.error(`Error when updating question: ${error}`);
throw error; throw error;
} }
} }
export const deleteQuestion = async (surveyId: number, id: number) => { export const deleteQuestion = async (id: number) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (!token) { if (!token) {
throw new Error("Токен отсутствует"); throw new Error("Токен отсутствует");
} }
try{ try{
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${id}`, { const response = await fetch(`${BASE_URL}/questions/${id}`, {
...createRequestConfig('DELETE'), ...createRequestConfig('DELETE'),
}) })
const responseData = await handleResponse(response); const responseData = await handleResponse(response);
@ -86,6 +83,7 @@ export const deleteQuestion = async (surveyId: number, id: number) => {
} }
return responseData; return responseData;
} catch (error){ } catch (error){
handleUnauthorizedError(error);
console.error(`Error deleting a question: ${error}`); console.error(`Error deleting a question: ${error}`);
throw error; throw error;
} }

View file

@ -1,4 +1,4 @@
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; import {BASE_URL, createRequestConfig, handleResponse, handleUnauthorizedError} from "./BaseApi.ts";
export interface ISurvey { export interface ISurvey {
id: number; id: number;
@ -28,6 +28,7 @@ export const getMySurveys = async (): Promise<ISurvey[]> => {
}); });
return await handleResponse(response); return await handleResponse(response);
} catch (error) { } catch (error) {
handleUnauthorizedError(error);
console.error("Error receiving surveys:", error); console.error("Error receiving surveys:", error);
throw error; throw error;
} }
@ -43,6 +44,7 @@ export const getAllSurveys = async (): Promise<ISurvey[]> => {
}) })
return await handleResponse(response); return await handleResponse(response);
} catch (error) { } catch (error) {
handleUnauthorizedError(error);
console.error("Error receiving surveys:", error); console.error("Error receiving surveys:", error);
throw error; throw error;
} }
@ -68,12 +70,13 @@ export const postNewSurvey = async (survey: INewSurvey): Promise<ISurvey> => {
throw new Error(`Ошибка: ${response.status}`); throw new Error(`Ошибка: ${response.status}`);
} }
const data = await response.json(); const data = await handleResponse(response);
if (!data.id) { if (!data.id) {
throw new Error("Сервер не вернул ID опроса"); throw new Error("Сервер не вернул ID опроса");
} }
return data; return data;
} catch (error) { } catch (error) {
handleUnauthorizedError(error);
console.error(`Error when adding a new survey: ${error}`); console.error(`Error when adding a new survey: ${error}`);
throw error; throw error;
} }
@ -90,6 +93,7 @@ export const getSurveyById = async (surveyId: number): Promise<ISurvey> => {
}) })
return await handleResponse(response); return await handleResponse(response);
} catch (error){ } catch (error){
handleUnauthorizedError(error);
console.error(`Error finding the survey by id: ${error}`); console.error(`Error finding the survey by id: ${error}`);
throw error; throw error;
} }
@ -115,6 +119,7 @@ export const deleteSurvey = async (surveyId: number) => {
} }
return responseData; return responseData;
} catch (error){ } catch (error){
handleUnauthorizedError(error);
console.error(`Error deleting a survey: ${error}`); console.error(`Error deleting a survey: ${error}`);
throw error; throw error;
} }
@ -138,12 +143,10 @@ export const updateSurvey = async (surveyId: number, survey: Partial<INewSurvey>
description: survey.description, description: survey.description,
}) })
}) })
if (response.status === 200) {
return await handleResponse(response); return await handleResponse(response);
} }
throw new Error(`Ожидался код 200, получен ${response.status}`);
}
catch (error){ catch (error){
handleUnauthorizedError(error);
console.error(`Error updating survey: ${error}`); console.error(`Error updating survey: ${error}`);
throw error; throw error;
} }

View file

@ -4,8 +4,8 @@
background-color: #F3F3F3; background-color: #F3F3F3;
border-radius: 40px; border-radius: 40px;
align-items: center; align-items: center;
padding: 4.58px 13px 4.58px 4.58px; padding: 2px 10px 2px 4.5px;
margin: 26px 33px 27px 0; margin: 15px 33px 15px 0;
margin-left: auto; margin-left: auto;
display: inline-flex; display: inline-flex;
max-width: 100%; max-width: 100%;
@ -18,7 +18,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: black; color: black;
text-decoration: none; text-decoration: none;
@ -27,7 +27,6 @@
.accountImg{ .accountImg{
vertical-align: middle; vertical-align: middle;
width: 55px; width: 40px;
margin-right: 9px;
flex-shrink: 0; flex-shrink: 0;
} }

View file

@ -11,28 +11,6 @@ const Account: React.FC<AccountProps> = ({ href }) => {
const [userName, setUserName] = useState<string>(); const [userName, setUserName] = useState<string>();
const [isLoading, setIsLoading] = useState(true); 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(() => { useEffect(() => {
const fetchUserData = async () => { const fetchUserData = async () => {
try { try {

View file

@ -3,15 +3,16 @@
.answerButton { .answerButton {
margin-top: 18px; margin-top: 18px;
display: flex; display: flex;
gap: 10px; gap: 8px;
align-items: center; align-items: center;
border: none; border: none;
background-color: white; background-color: white;
color: #3788D6; color: #3788D6;
font-size: 18px; font-size: 15px;
font-weight: 500; font-weight: 500;
} }
.addAnswerImg{ .addAnswerImg{
width: 12px;
vertical-align: middle; vertical-align: middle;
} }

View file

@ -4,7 +4,7 @@
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 80px; margin-bottom: 40px;
align-items: center; align-items: center;
background-color: #F6F6F6; background-color: #F6F6F6;
border: none; border: none;
@ -12,12 +12,13 @@
} }
.questionButtonImg{ .questionButtonImg{
width: 54px; width: 40px;
align-items: center; align-items: center;
margin-bottom: -15px;
} }
.textButton{ .textButton{
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
} }

View file

@ -4,19 +4,20 @@
width: 100%; width: 100%;
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-bottom: 17px; margin-bottom: 7px;
align-items: flex-start; align-items: flex-start;
} }
.textAnswer { .textAnswer {
text-align: left; text-align: left;
border: none; border: none;
outline: none;
background: none; background: none;
font-size: 18px; font-size: 15px;
font-weight: 500; font-weight: 500;
word-break: break-word; word-break: break-word;
width: 70%;
padding: 0; padding: 0;
margin-right: 150px;
line-height: 24px; line-height: 24px;
cursor: text; cursor: text;
margin-top: 2px; margin-top: 2px;
@ -38,20 +39,21 @@
} }
.answerIcon { .answerIcon {
width: 24px; width: 20px;
height: 24px; height: 20px;
display: block; display: block;
} }
.answerInput { .answerInput {
font-size: 18px; font-size: 15px;
font-weight: 500; font-weight: 500;
outline: none; outline: none;
border: none; border: none;
resize: none; resize: none;
width: 70%; width: 70%;
padding: 0; padding: 0;
margin-top: 2px; margin-top: 3px;
margin-bottom: -20px;
font-family: inherit; font-family: inherit;
min-height: 24px; min-height: 24px;
height: auto; height: auto;
@ -67,3 +69,7 @@
background-color: transparent; background-color: transparent;
padding: 0; padding: 0;
} }
.img{
width: 20px;
}

View file

@ -6,6 +6,7 @@ import Multiple from '../../assets/emptyCheckbox.svg?react';
import SelectedSingle from '../../assets/radio_button_checked.svg?react' import SelectedSingle from '../../assets/radio_button_checked.svg?react'
import SelectedMultiple from '../../assets/check_box.svg?react'; import SelectedMultiple from '../../assets/check_box.svg?react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts";
interface AnswerOptionProps{ interface AnswerOptionProps{
index: number; index: number;
@ -15,14 +16,14 @@ interface AnswerOptionProps{
selectedType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion'; selectedType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
isSelected?: boolean; isSelected?: boolean;
toggleSelect?: () => void; toggleSelect?: () => void;
isCompleteSurveyActive?: boolean;
} }
const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect, isCompleteSurveyActive = false}) => { const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect}) => {
const [currentValue, setCurrentValue] = useState(value); const [currentValue, setCurrentValue] = useState(value);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
const isReadOnly = useRouteReadOnly();
useEffect(() => { useEffect(() => {
setCurrentValue(value); setCurrentValue(value);
@ -71,7 +72,7 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
}, [isEditing]); }, [isEditing]);
const handleMarkerClick = () => { const handleMarkerClick = () => {
if (isCompleteSurveyActive && toggleSelect) { if (isReadOnly && toggleSelect) {
toggleSelect(); toggleSelect();
} }
}; };
@ -79,7 +80,7 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
return ( return (
<div className={styles.answer}> <div className={styles.answer}>
{isCompleteSurveyActive ? ( {isReadOnly ? (
<button <button
className={`${styles.buttonMarker} ${isSelected ? styles.selected : ''}`} className={`${styles.buttonMarker} ${isSelected ? styles.selected : ''}`}
onClick={handleMarkerClick} onClick={handleMarkerClick}
@ -108,7 +109,7 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
</button> </button>
)} )}
{isCompleteSurveyActive ? ( {isReadOnly ? (
<button className={styles.textAnswer}> <button className={styles.textAnswer}>
{currentValue || `Ответ ${index}`} {currentValue || `Ответ ${index}`}
</button> </button>
@ -128,9 +129,9 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
</button> </button>
)} )}
{!isCompleteSurveyActive && ( {!isReadOnly && (
<button className={styles.deleteButton} onClick={() => onDelete?.(index)}> <button className={styles.deleteButton} onClick={() => onDelete?.(index)}>
<Delete /> <Delete className={styles.img}/>
</button> </button>
)} )}
</div> </div>

View file

@ -5,3 +5,18 @@
min-height: 100vh; min-height: 100vh;
padding: 34px 16%; 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;
}

View file

@ -1,30 +1,117 @@
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx"; import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx";
import {useState} from "react"; import {useEffect, useState} from "react";
import styles from './CompletingSurvey.module.css' 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 = () => { export const CompletingSurvey = () => {
const [titleSurvey, setTitleSurvey] = useState("Название опроса"); const {surveyId} = useParams<{surveyId: string}>();
const [descriptionSurvey, setDescriptionSurvey] = useState(""); const [survey, setSurvey] = useState<ISurvey | null>(null);
const [questions, setQuestions] = useState<Question[]>([ const [questions, setQuestions] = useState<Question[]>([]);
{ id: 1, text: 'Вопрос 1', questionType: 'SingleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' }, const [loading, setLoading] = useState(true);
{ id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]}, const [error, setError] = useState<string | null>(null);
{ id: 2, text: 'Вопрос 2', questionType: 'MultipleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' },
{ id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]} const [selectedAnswers, setSelectedAnswers] = useState<ISelectedAnswers[]>([]);
]); 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 <div>Загрузка...</div>;
if (error) return <div>{error}</div>;
if (!survey) return <div>Опрос не найден</div>;
return ( return (
<div className={styles.survey}> <div className={styles.survey}>
<SurveyInfo <SurveyInfo
titleSurvey={titleSurvey} titleSurvey={survey.title}
descriptionSurvey={descriptionSurvey} descriptionSurvey={survey.description}
setDescriptionSurvey={setDescriptionSurvey} setDescriptionSurvey={(value) => setSurvey({ ...survey, description: value })}
setTitleSurvey={setTitleSurvey} setTitleSurvey={(value) => setSurvey({ ...survey, title: value })}
/> />
<QuestionsList <QuestionsList
questions={questions} questions={questions}
setQuestions={setQuestions} setQuestions={setQuestions}
onAnswerSelect={handleAnswerSelect}
/> />
<button className={styles.departur_button} onClick={handleSubmit} disabled={selectedAnswers.length === 0}>Отправить</button>
</div> </div>
) )
} }

View file

@ -5,18 +5,19 @@
padding: 0; padding: 0;
width: 100%; width: 100%;
display: flex; display: flex;
height: fit-content;
} }
.pagesNav{ .pagesNav{
display: flex; display: flex;
gap: 60px; gap: 80px;
list-style: none; list-style: none;
align-items: center; align-items: center;
margin-right: 20%; margin-right: 20%;
} }
.pageLink{ .pageLink{
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #2A6DAE; color: #2A6DAE;
padding: 0; padding: 0;

View file

@ -10,13 +10,12 @@ const Header: React.FC = () => {
const isCreateSurveyActive = location.pathname.startsWith('/survey/create'); const isCreateSurveyActive = location.pathname.startsWith('/survey/create');
const isMySurveysActive = location.pathname === '/my-surveys'; const isMySurveysActive = location.pathname === '/my-surveys';
const isCompleteSurveyActive = location.pathname === '/complete-survey';
const isSurveyViewPage = location.pathname.startsWith('/survey/') && const isSurveyViewPage = location.pathname.startsWith('/survey/') &&
!location.pathname.startsWith('/survey/create'); !location.pathname.startsWith('/survey/create');
const handleLogoClick = () => { const handleLogoClick = () => {
navigate(location.pathname, { replace: true }); navigate(location.pathname);
}; };
return ( return (
@ -37,13 +36,6 @@ const Header: React.FC = () => {
Мои опросы Мои опросы
{(isMySurveysActive || isSurveyViewPage) && <hr className={styles.activeLine}/>} {(isMySurveysActive || isSurveyViewPage) && <hr className={styles.activeLine}/>}
</Link> </Link>
<Link
to='/complete-survey'
className={`${styles.pageLink} ${isCompleteSurveyActive ? styles.active : ''}`}
>
Прохождение опроса
{isCompleteSurveyActive && <hr className={styles.activeLine}/>}
</Link>
</nav> </nav>
<Account href={'/profile'} /> <Account href={'/profile'} />
</div> </div>

View file

@ -1,19 +1,19 @@
.loginContainer{ .loginContainer{
width: 31%; width: 26%;
height: fit-content;
background-color: #FFFFFF; background-color: #FFFFFF;
padding: 42.5px 65px; padding: 42.5px 65px;
margin: auto; margin: 0 auto;
border-radius: 43px; border-radius: 43px;
margin-bottom: 0;
} }
.title{ .title{
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
font-size: 40px; font-size: 30px;
line-height: 88%; line-height: 88%;
padding: 0; padding: 0;
margin-bottom: 80px; margin-bottom: 60px;
margin-top: 0; margin-top: 0;
} }
@ -21,12 +21,12 @@
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 80px; gap: 50px;
margin-bottom: 80px; margin-bottom: 40px;
} }
.input { .input {
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; color: #000000;
@ -49,14 +49,14 @@
.errorMessage{ .errorMessage{
text-align: left; text-align: left;
font-size: 14px; font-size: 12px;
font-weight: 400; font-weight: 400;
line-height: 88%; line-height: 88%;
color: #C0231F; color: #C0231F;
} }
.input::placeholder { .input::placeholder {
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; color: #000000;
@ -78,12 +78,12 @@
.signIn{ .signIn{
margin: auto; margin: auto;
padding: 26.5px 67px; padding: 20px 40px;
width: fit-content; width: fit-content;
border-radius: 24px; border-radius: 24px;
background-color: #3788D6; background-color: #3788D6;
color: #FFFFFF; color: #FFFFFF;
font-size: 24px; font-size: 20px;
font-weight: 600; font-weight: 600;
line-height: 120%; line-height: 120%;
border: none; border: none;
@ -92,7 +92,7 @@
.recommendation{ .recommendation{
text-align: center; text-align: center;
font-size: 18px; font-size: 15px;
font-weight: 500; font-weight: 500;
} }

View file

@ -27,7 +27,7 @@ const LoginForm = () => {
else { else {
const responseData = await authUser({email, password}); const responseData = await authUser({email, password});
if (responseData && !responseData.error) if (responseData && !responseData.error)
navigate('/my-surveys'); navigate('/my-surveys', {replace: true});
else else
setError('Неверный логин или пароль') setError('Неверный логин или пароль')
} }

View file

@ -1,8 +1,13 @@
/*Logo.module.css*/ /*Logo.module.css*/
.logo { .logo {
outline: none;
padding: 0; padding: 0;
height: 52px; margin: 0 100px 0 40px;
width: 52px; display: flex;
margin: 31px 77px 25px 40px; align-items: center;
}
.logoImg{
outline: none;
width: 40px;
} }

View file

@ -10,7 +10,7 @@ interface LogoProps {
const Logo: React.FC<LogoProps> = ({href, onClick}) => { const Logo: React.FC<LogoProps> = ({href, onClick}) => {
return ( return (
<a className={styles.logo} href={href} onClick={onClick}> <a className={styles.logo} href={href} onClick={onClick}>
<LogoImg/> <LogoImg className={styles.logoImg}/>
</a> </a>
); );
}; };

View file

@ -12,7 +12,7 @@
background-color: white; background-color: white;
width: 79%; width: 79%;
border-radius: 14px; border-radius: 14px;
padding: 29px 36px 29px 54px; padding: 29px 36px 15px 54px;
margin-bottom: 23px; margin-bottom: 23px;
gap: 20px; gap: 20px;
border: none; border: none;
@ -35,6 +35,8 @@
.buttonDelete{ .buttonDelete{
border-radius: 8px; border-radius: 8px;
margin-top: 10px;
margin-left: 30px;
align-items: center; align-items: center;
background-color: #FFFFFF; background-color: #FFFFFF;
border: none; border: none;
@ -42,7 +44,7 @@
padding: 5px 3px; padding: 5px 3px;
color: black; color: black;
font-weight: 500; font-weight: 500;
font-size: 18px; font-size: 15px;
} }
.buttonDelete:hover{ .buttonDelete:hover{
@ -51,12 +53,13 @@
.imgDelete{ .imgDelete{
vertical-align: middle; vertical-align: middle;
width: 18px;
} }
.status { .status {
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
padding: 15px 47px; padding: 12px 35px;
border-radius: 15px; border-radius: 15px;
color: #FFFFFF; color: #FFFFFF;
white-space: nowrap; white-space: nowrap;
@ -65,30 +68,33 @@
.completed { .completed {
background-color: #B0B0B0; background-color: #B0B0B0;
font-size: 18px;
} }
.active { .active {
background-color: #65B953; background-color: #65B953;
font-size: 18px;
} }
.surveyData { .surveyData {
margin-bottom: 33px; margin-top: -15px;
margin-bottom: 15px;
} }
.title { .title {
font-size: 40px; font-size: 25px;
font-weight: 600; font-weight: 600;
word-break: break-word; word-break: break-word;
} }
.description { .description {
font-size: 24px; font-size: 17px;
font-weight: 500; font-weight: 500;
word-break: break-word; word-break: break-word;
} }
.date { .date {
font-size: 18px; font-size: 15px;
font-weight: 500; font-weight: 500;
color: #7D7983; color: #7D7983;
} }

View file

@ -6,12 +6,12 @@
} }
.nav{ .nav{
margin: 34px 0 48px 40px; margin: 34px 0 0 60px;
background-color: white; background-color: white;
border-radius: 20px; border-radius: 20px;
} }
.navList{ .navList{
list-style: none; list-style: none;
padding: 52px 57px 70px 36px; padding: 32px 37px 40px 26px;
} }

View file

@ -2,13 +2,14 @@
.navItem{ .navItem{
padding: 0; padding: 0;
margin-bottom: 42px; margin-bottom: 35px;
} }
.page{ .page{
background-color: white; background-color: white;
border: none; border: none;
font-size: 24px; outline: none;
font-size: 18px;
font-weight: 600; font-weight: 600;
color: #AFAFAF; color: #AFAFAF;
} }

View file

@ -4,7 +4,7 @@
background-color: white; background-color: white;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 34px; margin-bottom: 20px;
padding: 27px 29px 26px 36px; padding: 27px 29px 26px 36px;
border-radius: 14px; border-radius: 14px;
} }
@ -22,15 +22,17 @@
} }
.questionTextarea{ .questionTextarea{
font-family: Monserrat, sans-serif;
width: 70%; width: 70%;
align-items: center; align-items: center;
border: none; border: none;
outline: none; outline: none;
resize: none; resize: none;
margin-bottom: 5px; margin-bottom: 24px;
font-size: 24px; margin-left: -2px;
font-size: 18px;
margin-top: -2px;
font-weight: 600; font-weight: 600;
line-height: 1.5;
overflow-y: hidden; overflow-y: hidden;
min-height: 1em; min-height: 1em;
} }
@ -44,11 +46,12 @@
} }
.textQuestion{ .textQuestion{
min-height: 1em;
margin-top: 0; margin-top: 0;
width: 100%; width: 100%;
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
margin-bottom: 35px; margin-bottom: 30px;
text-align: start; text-align: start;
word-break: break-word; word-break: break-word;
} }
@ -59,7 +62,7 @@
} }
.deleteQuestionButton{ .deleteQuestionButton{
font-size: 18px; font-size: 15px;
font-weight: 500; font-weight: 500;
color: #EC221F; color: #EC221F;
border: none; border: none;
@ -72,6 +75,6 @@
.basketImg{ .basketImg{
vertical-align: middle; vertical-align: middle;
width: 24px; width: 20px;
color: #EC221F; color: #EC221F;
} }

View file

@ -9,9 +9,9 @@ import {
deleteAnswerVariant, deleteAnswerVariant,
getAnswerVariants, getAnswerVariants,
updateAnswerVariant updateAnswerVariant
} from "../../api/AnswerApi.ts"; } from "../../api/AnswerVariantsApi.ts";
import {useLocation} from "react-router-dom";
import TextareaAutosize from "react-textarea-autosize"; import TextareaAutosize from "react-textarea-autosize";
import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts";
interface QuestionItemProps { interface QuestionItemProps {
questionId: number; questionId: number;
@ -23,8 +23,8 @@ interface QuestionItemProps {
onDeleteQuestion: (index: number) => Promise<void>; onDeleteQuestion: (index: number) => Promise<void>;
initialQuestionType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion'; initialQuestionType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
onQuestionTypeChange: (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => void; onQuestionTypeChange: (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => void;
surveyId?: number; surveyId?: number;
onAnswerSelect?: (questionId: number, answerText: string) => void;
} }
const QuestionItem: React.FC<QuestionItemProps> = ({ const QuestionItem: React.FC<QuestionItemProps> = ({
@ -37,7 +37,8 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
onDeleteQuestion, onDeleteQuestion,
initialQuestionType, initialQuestionType,
onQuestionTypeChange, onQuestionTypeChange,
surveyId surveyId,
onAnswerSelect
}) => { }) => {
const [textQuestion, setTextQuestion] = useState(initialTextQuestion); const [textQuestion, setTextQuestion] = useState(initialTextQuestion);
const [isEditingQuestion, setIsEditingQuestion] = useState(false); const [isEditingQuestion, setIsEditingQuestion] = useState(false);
@ -45,8 +46,7 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
const [questionType, setQuestionType] = useState<'SingleAnswerQuestion' | 'MultipleAnswerQuestion'>(initialQuestionType); const [questionType, setQuestionType] = useState<'SingleAnswerQuestion' | 'MultipleAnswerQuestion'>(initialQuestionType);
const textareaQuestionRef = useRef<HTMLTextAreaElement>(null); const textareaQuestionRef = useRef<HTMLTextAreaElement>(null);
const location = useLocation(); const isReadOnly = useRouteReadOnly();
const isCompleteSurveyActive = location.pathname === '/complete-survey';
useEffect(() => { useEffect(() => {
@ -63,6 +63,8 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
}; };
const handleAddAnswer = async () => { const handleAddAnswer = async () => {
if (isReadOnly) return
if (!surveyId) { if (!surveyId) {
onAnswerVariantsChange([...initialAnswerVariants, { text: '' }]); onAnswerVariantsChange([...initialAnswerVariants, { text: '' }]);
return; return;
@ -123,8 +125,6 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
if (surveyId && newAnswerVariants[index].id) { if (surveyId && newAnswerVariants[index].id) {
try { try {
await updateAnswerVariant( await updateAnswerVariant(
surveyId,
questionId,
newAnswerVariants[index].id!, newAnswerVariants[index].id!,
{ text: value } { text: value }
); );
@ -139,7 +139,7 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
if (surveyId && answerToDelete.id) { if (surveyId && answerToDelete.id) {
try { try {
await deleteAnswerVariant(surveyId, questionId, answerToDelete.id); await deleteAnswerVariant(answerToDelete.id);
const newAnswerVariants = initialAnswerVariants.filter((_, i) => i !== index); const newAnswerVariants = initialAnswerVariants.filter((_, i) => i !== index);
onAnswerVariantsChange(newAnswerVariants); onAnswerVariantsChange(newAnswerVariants);
setSelectedAnswers(selectedAnswers.filter((i) => i !== index)); setSelectedAnswers(selectedAnswers.filter((i) => i !== index));
@ -167,12 +167,28 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
} }
}; };
// 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 toggleSelect = (index: number) => {
const answerText = initialAnswerVariants[index].text;
if (onAnswerSelect) {
onAnswerSelect(questionId, answerText);
}
if (initialQuestionType === 'SingleAnswerQuestion') { if (initialQuestionType === 'SingleAnswerQuestion') {
// Для одиночного выбора: заменяем массив одним выбранным индексом
setSelectedAnswers([index]); setSelectedAnswers([index]);
} else { } else {
// Для множественного выбора: добавляем/удаляем индекс
setSelectedAnswers(prev => setSelectedAnswers(prev =>
prev.includes(index) prev.includes(index)
? prev.filter(i => i !== index) ? prev.filter(i => i !== index)
@ -183,7 +199,7 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
return ( return (
<div className={styles.questionCard}> <div className={styles.questionCard}>
{isCompleteSurveyActive ? ( {isReadOnly ? (
<div> <div>
<div className={styles.questionContainer}> <div className={styles.questionContainer}>
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2> <h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
@ -196,7 +212,6 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
value={answer.text} value={answer.text}
isSelected={selectedAnswers.includes(index)} isSelected={selectedAnswers.includes(index)}
toggleSelect={() => toggleSelect(index)} toggleSelect={() => toggleSelect(index)}
isCompleteSurveyActive={isCompleteSurveyActive}
/> />
))} ))}
</div> </div>

View file

@ -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;
}

View file

@ -2,14 +2,14 @@ import React from "react";
import QuestionItem from "../QuestionItem/QuestionItem.tsx"; import QuestionItem from "../QuestionItem/QuestionItem.tsx";
import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx"; import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx";
import {addNewQuestion, deleteQuestion, getListQuestions} from "../../api/QuestionApi.ts"; import {addNewQuestion, deleteQuestion, getListQuestions} from "../../api/QuestionApi.ts";
import {addNewAnswerVariant} from "../../api/AnswerApi.ts"; import {addNewAnswerVariant} from "../../api/AnswerVariantsApi.ts";
import {useLocation} from "react-router-dom"; import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts";
import styles from './QuestionsList.module.css'
interface QuestionsListProps { interface QuestionsListProps {
questions: Question[]; questions: Question[];
setQuestions: (questions: Question[]) => void; setQuestions: (questions: Question[]) => void;
surveyId?: number; surveyId?: number;
onAnswerSelect?: (questionId: number, answerText: string) => void;
} }
export interface Question { export interface Question {
@ -22,9 +22,8 @@ export interface Question {
}>; }>;
} }
const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, surveyId}) => { const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, surveyId, onAnswerSelect}) => {
const location = useLocation(); const isReadOnly = useRouteReadOnly();
const isCompleteSurveyActive = location.pathname === '/complete-survey';
const handleAddQuestion = async () => { const handleAddQuestion = async () => {
if (!surveyId) { if (!surveyId) {
@ -72,7 +71,7 @@ const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, s
if (surveyId) { if (surveyId) {
const listQuestions = await getListQuestions(surveyId); const listQuestions = await getListQuestions(surveyId);
if (listQuestions.find(q => q.id === id)) { if (listQuestions.find(q => q.id === id)) {
const response = await deleteQuestion(surveyId, id); const response = await deleteQuestion(id);
if (!response?.success) { if (!response?.success) {
throw new Error('Не удалось удалить вопрос на сервере'); throw new Error('Не удалось удалить вопрос на сервере');
} }
@ -126,11 +125,10 @@ const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, s
initialQuestionType={question.questionType} initialQuestionType={question.questionType}
onQuestionTypeChange={(type) => handleQuestionTypeChange(question.id, type)} onQuestionTypeChange={(type) => handleQuestionTypeChange(question.id, type)}
surveyId={surveyId} surveyId={surveyId}
onAnswerSelect={onAnswerSelect}
/> />
))} ))}
{!isCompleteSurveyActive ? <AddQuestionButton onClick={handleAddQuestion} /> : ( {!isReadOnly ? <AddQuestionButton onClick={handleAddQuestion} /> : ''}
<button className={styles.departur_button}>Отправить</button>
)}
</> </>
); );

View file

@ -1,18 +1,19 @@
.registerContainer{ .registerContainer{
width: 31%; width: 26%;
height: fit-content;
background-color: #FFFFFF; background-color: #FFFFFF;
padding: 94px 80px; padding: 60px 50px;
margin: auto; margin: 0 auto;
border-radius: 43px; border-radius: 43px;
} }
.title{ .title{
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
font-size: 40px; font-size: 30px;
line-height: 88%; line-height: 88%;
padding: 0; padding: 0;
margin-bottom: 80px; margin-bottom: 60px;
margin-top: 0; margin-top: 0;
} }
@ -20,12 +21,12 @@
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 80px; gap: 50px;
margin-bottom: 80px; margin-bottom: 40px;
} }
.input { .input {
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; color: #000000;
@ -37,7 +38,7 @@
} }
.input::placeholder { .input::placeholder {
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
line-height: 88%; line-height: 88%;
color: #000000; color: #000000;
@ -59,12 +60,12 @@
.signUp{ .signUp{
margin: auto; margin: auto;
padding: 25.5px 16px; padding: 22.5px 14px;
width: fit-content; width: fit-content;
border-radius: 24px; border-radius: 24px;
background-color: #3788D6; background-color: #3788D6;
color: #FFFFFF; color: #FFFFFF;
font-size: 24px; font-size: 20px;
font-weight: 600; font-weight: 600;
line-height: 120%; line-height: 120%;
border: none; border: none;
@ -73,8 +74,9 @@
.recommendation{ .recommendation{
text-align: center; text-align: center;
font-size: 18px; font-size: 14px;
font-weight: 500; font-weight: 500;
margin-bottom: 0;
} }
.recommendationLink{ .recommendationLink{
@ -94,7 +96,7 @@
.errorMessage{ .errorMessage{
text-align: left; text-align: left;
font-size: 14px; font-size: 12px;
font-weight: 400; font-weight: 400;
line-height: 88%; line-height: 88%;
color: #C0231F; color: #C0231F;

View file

@ -38,11 +38,8 @@ const RegisterForm = () => {
const responseData = await registerUser({username, firstName, lastName, email, password}); const responseData = await registerUser({username, firstName, lastName, email, password});
if (responseData && !responseData.error) { if (responseData && !responseData.error) {
console.log('Регистрация успешна'); console.log('Регистрация успешна');
localStorage.setItem("user", JSON.stringify({ localStorage.setItem("user", JSON.stringify(responseData.user));
firstName, navigate('/my-surveys', {replace: true});
lastName
}));
navigate('/my-surveys');
} }
else if (responseData.status === 409){ else if (responseData.status === 409){
setError('Аккаунт с такой почтой уже зарегистрирован'); setError('Аккаунт с такой почтой уже зарегистрирован');

View file

@ -9,7 +9,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: stretch; align-items: stretch;
margin: 30px 0; margin: 20px 0;
gap: 17px; gap: 17px;
} }
@ -17,7 +17,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: 15px; border-radius: 15px;
min-height: 180px; min-height: 140px;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
@ -43,9 +43,9 @@
} }
.statItem h3 { .statItem h3 {
margin: 0 0 15px 0; margin: 0 0 0 0;
color: #FFFFFF; color: #FFFFFF;
font-size: 28px; font-size: 20px;
font-weight: 600; font-weight: 600;
line-height: 1.2; line-height: 1.2;
} }
@ -68,20 +68,20 @@
.countAnswer p, .countAnswer p,
.completion_percentage p { .completion_percentage p {
font-size: 60px; margin-bottom: -10px;
font-size: 35px;
} }
.imgGroup, .imgGroup,
.imgSend { .imgSend {
width: 58px; width: 43px;
height: 61px;
align-self: flex-end; align-self: flex-end;
} }
.status p { .status p {
text-align: center; text-align: center;
margin-top: auto; margin-top: auto;
font-size: 32px; font-size: 26px;
} }
.questionContainer { .questionContainer {
@ -106,13 +106,13 @@
.textContainer { .textContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 11px; gap: 5px;
width: 30%; width: 30%;
min-width: 250px; min-width: 250px;
} }
.questionContainer h3 { .questionContainer h3 {
font-size: 24px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: #000000; color: #000000;
margin: 0; margin: 0;
@ -120,7 +120,7 @@
.answerCount { .answerCount {
color: #000000; color: #000000;
font-size: 18px; font-size: 16px;
font-weight: 600; font-weight: 600;
} }
@ -133,16 +133,31 @@
} }
.pieContainer { .pieContainer {
width: 100%; width: 70%;
height: 450px;
position: relative; position: relative;
} }
.barContainer { .barContainer {
width: 100%; width: 100%;
height: 450px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding-right: 150px; 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;
}

View file

@ -3,66 +3,170 @@ import styles from './Results.module.css';
import {Bar, Pie} from 'react-chartjs-2'; import {Bar, Pie} from 'react-chartjs-2';
import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js'; import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js';
import {useOutletContext} from "react-router-dom"; 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 ChartDataLabels from 'chartjs-plugin-datalabels';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import Group from '../../assets/gmail_groups.svg?react'; import Group from '../../assets/gmail_groups.svg?react';
import Send from '../../assets/send.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( ChartJS.register(
ArcElement, Tooltip, Legend, ArcElement, Tooltip, Legend,
CategoryScale, LinearScale, BarElement, Title, ChartDataLabels, annotationPlugin CategoryScale, LinearScale, BarElement, Title, ChartDataLabels, annotationPlugin
); );
// Типы для данных
interface QuestionStats { interface QuestionStats {
questionText: string; questionText: string;
totalAnswers: number; totalAnswers: number;
options: { options: {
text: string; text: string;
percentage: number; percentage: number;
id: number;
}[]; }[];
isMultipleChoice?: boolean; isMultipleChoice: boolean;
}
interface IAnswer {
questionId: number;
completionId: number;
answerText: string;
optionId?: number;
} }
export const Results = () => { export const Results = () => {
const { survey, setSurvey } = useOutletContext<{ const { survey: initialSurvey, setSurvey } = useOutletContext<{
survey: ISurvey; survey: ISurvey;
setSurvey: (survey: ISurvey) => void; setSurvey: (survey: ISurvey) => void;
}>(); }>();
const [surveyStats, setSurveyStats] = useState({
const surveyStats = { totalParticipants: 0,
totalParticipants: 100, completionPercentage: 0,
completionPercentage: 80,
status: 'Активен', status: 'Активен',
questions: [ questions: [] as QuestionStats[]
{ });
questionText: "Вопрос 1",
totalAnswers: 80, const [survey, setLocalSurvey] = useState<ISurvey>(initialSurvey);
options: [ // const [questions, setQuestions] = useState<IQuestion[]>([]);
{ text: "Вариант 1", percentage: 46 },
{ text: "Вариант 2", percentage: 15 }, const handleExportToExcel = async (id: number) => {
{ text: "Вариант 3", percentage: 39 } await getResultsFile(id)
],
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[]
}; };
// Цветовая палитра 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<number, IAnswer[]>);
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 colorsForPie = ['#67C587', '#C9EAD4', '#EAF6ED'];
const colorsForBar = ['#8979FF']; const colorsForBar = ['#8979FF'];
@ -71,8 +175,14 @@ export const Results = () => {
<SurveyInfo <SurveyInfo
titleSurvey={survey.title} titleSurvey={survey.title}
descriptionSurvey={survey.description} descriptionSurvey={survey.description}
setDescriptionSurvey={(value) => setSurvey({ ...survey, description: value })} setDescriptionSurvey={(value) => {
setTitleSurvey={(value) => setSurvey({ ...survey, title: value })} setSurvey({ ...survey, description: value });
setLocalSurvey({ ...survey, description: value });
}}
setTitleSurvey={(value) => {
setSurvey({ ...survey, title: value });
setLocalSurvey({ ...survey, title: value });
}}
/> />
<div className={styles.statsContainer}> <div className={styles.statsContainer}>
<div className={`${styles.statItem} ${styles.countAnswer}`}> <div className={`${styles.statItem} ${styles.countAnswer}`}>
@ -133,7 +243,7 @@ export const Results = () => {
xValue: i, xValue: i,
yValue: opt.percentage + 5, yValue: opt.percentage + 5,
content: `${opt.percentage}%`, content: `${opt.percentage}%`,
font: { size: 16, weight: 400 }, font: { size: 1, weight: 400 },
color: '#000' color: '#000'
})) }))
} }
@ -148,7 +258,7 @@ export const Results = () => {
ticks: { ticks: {
color: '#000000', color: '#000000',
font: { font: {
size: 16, size: 12,
weight: 400 weight: 400
} }
}, },
@ -178,7 +288,7 @@ export const Results = () => {
labels: { labels: {
color: '#000000', color: '#000000',
font: { font: {
size: 18, size: 12,
weight: 500 weight: 500
} }
} }
@ -191,7 +301,7 @@ export const Results = () => {
datalabels: { datalabels: {
formatter: (value) => `${value}%`, formatter: (value) => `${value}%`,
color: '#000', color: '#000',
font: { weight: 400, size: 16 } font: { weight: 400, size: 12 }
} }
}, },
animation: { animateRotate: true } animation: { animateRotate: true }
@ -203,6 +313,8 @@ export const Results = () => {
</div> </div>
</div> </div>
))} ))}
<button className={styles.exportButtonContainer} onClick={() => handleExportToExcel(survey.id)}>Экспорт в excel</button>
</div> </div>
); );
}; };

View file

@ -2,14 +2,14 @@
.createSurveyButton { .createSurveyButton {
display: block; display: block;
margin: 10px auto; margin: 0 auto;
padding: 25px 50.5px; padding: 20px 35px;
border: none; border: none;
border-radius: 20px; border-radius: 20px;
background-color: #3788D6; background-color: #3788D6;
color: white; color: white;
font-weight: 700; font-weight: 700;
font-size: 24px; font-size: 18px;
text-align: center; text-align: center;
box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1); box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1);
box-sizing: border-box; box-sizing: border-box;

View file

@ -7,7 +7,7 @@ interface CreateSurveyButtonProps {
const SaveButton: React.FC<CreateSurveyButtonProps> = ({onClick}) => { const SaveButton: React.FC<CreateSurveyButtonProps> = ({onClick}) => {
return ( return (
<button onClick={onClick} className={styles.createSurveyButton}> <button onClick={onClick} className={styles.createSurveyButton} >
Сохранить Сохранить
</button> </button>
); );

View file

@ -14,14 +14,31 @@
.param{ .param{
border-radius: 4px; border-radius: 4px;
background-color: #FFFFFF; background-color: #FFFFFF;
padding-top: 15px; padding-top: 8px;
padding-bottom: 97px; padding-bottom: 60px;
padding-left: 19px; padding-left: 19px;
margin-bottom: 30px; margin-bottom: 30px;
} }
.param h2{ .param h2{
font-size: 24px; font-size: 18px;
font-weight: 600; font-weight: 600;
border-radius: 4px; 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;
}

View file

@ -1,10 +1,11 @@
import React, {useState} from 'react'; import React from 'react';
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import styles from "./SettingSurvey.module.css"; import styles from "./SettingSurvey.module.css";
import TimeEvent from "../TimeEvent/TimeEvent.tsx"; import TimeEvent from "../TimeEvent/TimeEvent.tsx";
import SaveButton from "../SaveButton/SaveButton.tsx"; import SaveButton from "../SaveButton/SaveButton.tsx";
import {ISurvey} from "../../api/SurveyApi.ts"; import {ISurvey} from "../../api/SurveyApi.ts";
import {useLocation, useOutletContext} from "react-router-dom"; import {useLocation, useOutletContext} from "react-router-dom";
import {useSurveyContext} from "../../context/SurveyContext.tsx";
const SettingSurvey: React.FC = () => { const SettingSurvey: React.FC = () => {
@ -14,18 +15,25 @@ const SettingSurvey: React.FC = () => {
survey: ISurvey; survey: ISurvey;
setSurvey: (survey: ISurvey) => void; setSurvey: (survey: ISurvey) => void;
}>(); }>();
const { tempSurvey, setTempSurvey } = useSurveyContext();
const [descriptionSurvey, setDescriptionSurvey] = useState(''); const handleCopyLink = () => {
const [titleSurvey, setTitleSurvey] = useState(''); 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 ( return (
<div className={styles.settingSurvey}> <div className={styles.settingSurvey}>
{isSettingCreatePage ? ( {isSettingCreatePage ? (
<SurveyInfo <SurveyInfo
titleSurvey={titleSurvey} titleSurvey={tempSurvey.title}
descriptionSurvey={descriptionSurvey} descriptionSurvey={tempSurvey.description}
setDescriptionSurvey={setDescriptionSurvey} setDescriptionSurvey={(value) => setTempSurvey({ ...tempSurvey, description: value })}
setTitleSurvey={setTitleSurvey} setTitleSurvey={(value) => setTempSurvey({ ...tempSurvey, title: value })}
/> />
) : ( ) : (
<SurveyInfo <SurveyInfo
@ -43,6 +51,7 @@ const SettingSurvey: React.FC = () => {
<h2>Параметры видимости</h2> <h2>Параметры видимости</h2>
</div> </div>
<SaveButton onClick={() => {}}/> <SaveButton onClick={() => {}}/>
{!isSettingCreatePage ? <button onClick={handleCopyLink} className={styles.copyButton}>Копировать ссылку</button> : ''}
</div> </div>
) )
} }

View file

@ -1,64 +1,78 @@
import React, {useState} from "react"; import React, { useState, useEffect } from "react";
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx"; import QuestionsList, { Question } from "../QuestionsList/QuestionsList.tsx";
import styles from './Survey.module.css' import styles from './Survey.module.css';
import SaveButton from "../SaveButton/SaveButton.tsx"; import SaveButton from "../SaveButton/SaveButton.tsx";
import {ISurvey, postNewSurvey} from "../../api/SurveyApi.ts"; import { ISurvey, postNewSurvey } from "../../api/SurveyApi.ts";
import {addNewQuestion} from "../../api/QuestionApi.ts"; import { addNewQuestion } from "../../api/QuestionApi.ts";
import {useNavigate} from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import {addNewAnswerVariant} from "../../api/AnswerApi.ts"; import { addNewAnswerVariant } from "../../api/AnswerVariantsApi.ts";
import { useSurveyContext } from "../../context/SurveyContext.tsx";
const Survey: React.FC = () => { const Survey: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [descriptionSurvey, setDescriptionSurvey] = useState(''); const location = useLocation();
const [titleSurvey, setTitleSurvey] = useState('Название опроса'); const { tempSurvey, setTempSurvey, clearTempSurvey } = useSurveyContext();
const [survey] = useState<ISurvey | null>(null); const [survey] = useState<ISurvey | null>(null);
const [questions, setQuestions] = useState<Question[]>([ const [questions, setQuestions] = useState<Question[]>(
{ id: 1, text: '', questionType: 'SingleAnswerQuestion', answerVariants: [{ text: '' }]}, 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 () => { const handleSave = async () => {
try { try {
const savedSurvey = await postNewSurvey({ const savedSurvey = await postNewSurvey({
title: titleSurvey, title: tempSurvey.title,
description: descriptionSurvey description: tempSurvey.description,
}); });
const updatedQuestions: Question[] = []; const questionPromises = questions.map(async (question) => {
for (const question of questions) {
const newQuestion = await addNewQuestion(savedSurvey.id, { const newQuestion = await addNewQuestion(savedSurvey.id, {
title: question.text, title: question.text,
questionType: question.questionType questionType: question.questionType,
}); });
const updatedQuestion: Question = { if (question.answerVariants?.length > 0) {
...question, await Promise.all(
id: newQuestion.id,
answerVariants: []
};
if (question.answerVariants && question.answerVariants.length > 0) {
const newVariants = await Promise.all(
question.answerVariants.map(answer => question.answerVariants.map(answer =>
addNewAnswerVariant( addNewAnswerVariant(savedSurvey.id, newQuestion.id, { text: answer.text })
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'); navigate('/my-surveys');
} catch (error) { } catch (error) {
console.error('Ошибка при сохранении:', error); console.error('Ошибка при сохранении:', error);
@ -68,20 +82,19 @@ const Survey: React.FC = () => {
return ( return (
<div className={styles.survey}> <div className={styles.survey}>
<SurveyInfo <SurveyInfo
titleSurvey={titleSurvey} titleSurvey={tempSurvey.title || "Название опроса"}
descriptionSurvey={descriptionSurvey} descriptionSurvey={tempSurvey.description}
setDescriptionSurvey={setDescriptionSurvey} setDescriptionSurvey={(value) => setTempSurvey({ ...tempSurvey, description: value })}
setTitleSurvey={setTitleSurvey} setTitleSurvey={(value) => setTempSurvey({ ...tempSurvey, title: value })}
/> />
<QuestionsList <QuestionsList
questions={questions} questions={questions}
setQuestions={setQuestions} setQuestions={setQuestions}
surveyId={survey?.id} surveyId={survey?.id}
/> />
<SaveButton onClick={handleSave} />
<SaveButton onClick={handleSave}/>
</div> </div>
); );
} };
export default Survey; export default Survey;

View file

@ -5,33 +5,30 @@
padding: 0; padding: 0;
width: 100%; width: 100%;
margin-top: 34px; margin-top: 34px;
margin-bottom: 49px; margin-bottom: 30px;
border-radius: 14px; border-radius: 14px;
/*min-height: 191px;*/
/*max-height: 100vh;*/
max-height: fit-content; max-height: fit-content;
} }
.info{ .info{
min-width: 373px; min-width: 300px;
/*display: block;*/ padding: 20px 35px;
padding: 35px; display: flex;
display: flex; /* Добавляем flex */ flex-direction: column;
flex-direction: column; /* Элементы в колонку */
align-items: center; align-items: center;
} }
.titleSurvey{ .titleSurvey{
min-height: 60px;
width: 80%; width: 80%;
display: block; display: block;
border: none; border: none;
outline: none;
margin: 0 auto 13px; margin: 0 auto 13px;
background-color: white; background-color: white;
text-align: center; text-align: center;
font-size: 20px; font-size: 26px;
font-weight: 600; font-weight: 600;
/*margin-bottom: 23px;*/
/*margin-bottom: 15px;*/
word-break: break-word; word-break: break-word;
padding: 0; padding: 0;
} }
@ -52,7 +49,9 @@
} }
.textareaTitle { .textareaTitle {
font-size: 32px; margin-top: 14px;
margin-bottom: -1px;
font-size: 26px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
line-height: 1.2; line-height: 1.2;
@ -60,11 +59,13 @@
} }
.textareaDescrip { .textareaDescrip {
font-size: 18px; font-size: 16px;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
line-height: 1.4; line-height: 1.4;
min-height: 24px; min-height: 24px;
margin-top: -2px;
margin-bottom: -3px;
} }
.descriptionWrapper { .descriptionWrapper {
@ -74,8 +75,10 @@
} }
.description { .description {
min-height: 24px;
border: none; border: none;
font-size: 24px; outline: none;
font-size: 16px;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
background-color: white; background-color: white;
@ -87,7 +90,8 @@
} }
.desc{ .desc{
font-size: 24px; font-size: 20px;
outline: none;
font-weight: 500; font-weight: 500;
background-color: white; background-color: white;
max-width: 80%; max-width: 80%;
@ -105,12 +109,12 @@
.descButtonImg{ .descButtonImg{
vertical-align: middle; vertical-align: middle;
width: 28px; width: 20px;
} }
.textButton{ .textButton{
vertical-align: middle; vertical-align: middle;
font-size: 24px; font-size: 16px;
font-weight: 500; font-weight: 500;
color: #7D7983; color: #7D7983;
padding: 10px; padding: 10px;
@ -118,7 +122,7 @@
.createdAt{ .createdAt{
text-align: center; text-align: center;
font-size: 18px; font-size: 15px;
font-weight: 500; font-weight: 500;
color: #7D7983; color: #7D7983;
} }

View file

@ -3,6 +3,7 @@ import styles from './SurveyInfo.module.css'
import AddDescripImg from '../../assets/add_circle.svg?react'; import AddDescripImg from '../../assets/add_circle.svg?react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import {useLocation} from "react-router-dom"; import {useLocation} from "react-router-dom";
import {useRouteReadOnly} from "../../hooks/useRouteReadOnly.ts";
interface SurveyInfoProps { interface SurveyInfoProps {
@ -20,10 +21,11 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null); const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
const location = useLocation(); const location = useLocation();
const isCompleteSurveyActive = location.pathname === '/complete-survey';
const isSurveyViewPage = location.pathname.startsWith('/survey/') && const isSurveyViewPage = location.pathname.startsWith('/survey/') &&
!location.pathname.startsWith('/survey/create'); !location.pathname.startsWith('/survey/create');
const isReadOnly = useRouteReadOnly();
const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => { const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescriptionSurvey?.(descripEvent.target.value); setDescriptionSurvey?.(descripEvent.target.value);
}; };
@ -88,17 +90,15 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
} }
const renderTitle = () => { const renderTitle = () => {
if (isCompleteSurveyActive) { if (isReadOnly) {
return ( return (
<button className={styles.titleSurvey}> <button className={styles.titleSurvey}>{titleSurvey || 'Название опроса'}</button>
<h1>{titleSurvey || 'Название опроса'}</h1>
</button>
) )
} }
if (showNewTitleField) { if (showNewTitleField) {
return ( return (
<h1 className={styles.titleSurvey}> // <h1 className={styles.titleSurvey}>
<TextareaAutosize <TextareaAutosize
className={styles.textareaTitle} className={styles.textareaTitle}
ref={titleTextareaRef} ref={titleTextareaRef}
@ -108,22 +108,20 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
onKeyDown={handleTitleKeyDown} onKeyDown={handleTitleKeyDown}
onBlur={handleTitleBlur} onBlur={handleTitleBlur}
/> />
</h1> // </h1>
); );
} }
return ( return (
<button className={styles.titleSurvey} onClick={handleAddNewTitleClick}> <button className={styles.titleSurvey} onClick={handleAddNewTitleClick}>{titleSurvey || 'Название опроса'}</button>
<h1>{titleSurvey || 'Название опроса'}</h1>
</button>
); );
}; };
const renderDescription = () => { const renderDescription = () => {
if (isCompleteSurveyActive) { if (isReadOnly) {
return descriptionSurvey ? ( return descriptionSurvey ? (
<p className={styles.desc}>{descriptionSurvey}</p> <p className={styles.desc}>{descriptionSurvey}</p>
) : 'Описание'; ) : '';
} }
if (descriptionSurvey && !showDescriptionField) { if (descriptionSurvey && !showDescriptionField) {
@ -167,7 +165,7 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
{renderTitle()} {renderTitle()}
{renderDescription()} {renderDescription()}
{(isSurveyViewPage || isCompleteSurveyActive) && createdAt && ( {(isSurveyViewPage || isReadOnly) && createdAt && (
<p className={styles.createdAt}>Дата создания: {addDate()}</p> <p className={styles.createdAt}>Дата создания: {addDate()}</p>
)} )}
</div> </div>

View file

@ -6,5 +6,5 @@
color: #C0231F; color: #C0231F;
text-align: center; text-align: center;
margin: 10px 0; margin: 10px 0;
font-size: 18px; font-size: 15px;
} }

View file

@ -6,7 +6,7 @@ import {useOutletContext} from "react-router-dom";
import { addNewQuestion, getListQuestions, updateQuestion, deleteQuestion } from "../../api/QuestionApi.ts"; import { addNewQuestion, getListQuestions, updateQuestion, deleteQuestion } from "../../api/QuestionApi.ts";
import styles from "./SurveyPage.module.css"; import styles from "./SurveyPage.module.css";
import SaveButton from "../SaveButton/SaveButton.tsx"; 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 = type ActionType =
| 'update-survey' | 'update-survey'
@ -118,14 +118,14 @@ class ActionQueue {
} }
private async handleUpdateQuestion(data: QuestionActionData & { id: number }) { private async handleUpdateQuestion(data: QuestionActionData & { id: number }) {
return await updateQuestion(data.surveyId, data.id, { return await updateQuestion(data.id, {
title: data.title, title: data.title,
questionType: data.questionType questionType: data.questionType
}); });
} }
private async handleDeleteQuestion(data: QuestionActionData & { id: number }) { private async handleDeleteQuestion(data: QuestionActionData & { id: number }) {
return await deleteQuestion(data.surveyId, data.id); return await deleteQuestion(data.id);
} }
private async handleCreateAnswer(data: AnswerActionData) { private async handleCreateAnswer(data: AnswerActionData) {
@ -136,7 +136,7 @@ class ActionQueue {
private async handleUpdateAnswer(data: AnswerActionData & { id: number }) { private async handleUpdateAnswer(data: AnswerActionData & { id: number }) {
try { try {
const result = await updateAnswerVariant(data.surveyId, data.questionId, data.id, { const result = await updateAnswerVariant(data.id, {
text: data.text text: data.text
}); });
return result; return result;
@ -148,16 +148,14 @@ class ActionQueue {
} }
private async handleDeleteAnswer(data: AnswerActionData & { id: number }) { 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 = () => { export const SurveyPage: React.FC = () => {
// const [survey, setSurvey] = useState<ISurvey | null>(null);
const [questions, setQuestions] = useState<Question[]>([]); const [questions, setQuestions] = useState<Question[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// const { surveyId } = useParams<{ surveyId: string }>();
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
@ -173,7 +171,6 @@ export const SurveyPage: React.FC = () => {
return; return;
} }
// const id = parseInt(survey.id);
const id = survey.id; const id = survey.id;
if (isNaN(id)) { if (isNaN(id)) {
console.error('Invalid survey ID'); console.error('Invalid survey ID');
@ -183,7 +180,6 @@ export const SurveyPage: React.FC = () => {
const fetchData = async () => { const fetchData = async () => {
try { try {
setLoading(true); setLoading(true);
// const surveyData = await getSurveyById(id);
setSurvey(survey); setSurvey(survey);
setTitle(survey.title); setTitle(survey.title);
setDescription(survey.description); setDescription(survey.description);
@ -218,7 +214,6 @@ export const SurveyPage: React.FC = () => {
try { try {
setError(null); setError(null);
// const id = parseInt(survey.id);
const id = survey.id; const id = survey.id;
const actionQueue = new ActionQueue(); const actionQueue = new ActionQueue();

View file

@ -3,7 +3,7 @@
.timeEvent{ .timeEvent{
width: 44%; width: 44%;
padding: 17px 25px 48px 20px; padding: 8px 25px 28px 20px;
background-color: #FFFFFF; background-color: #FFFFFF;
border-radius: 6px; border-radius: 6px;
margin-bottom: 34px; margin-bottom: 34px;
@ -11,7 +11,7 @@
.title{ .title{
font-weight: 600; font-weight: 600;
font-size: 24px; font-size: 18px;
margin-bottom: 23px; margin-bottom: 23px;
} }
@ -24,8 +24,9 @@
.inputDate{ .inputDate{
width: fit-content; width: fit-content;
border: 3px solid #007AFF26; border: 3px solid #007AFF26;
padding: 12px 40px 12px 21px; padding: 8px 25px 8px 21px;
font-size: 20px; font-family: inherit;
font-size: 15px;
font-weight: 400; font-weight: 400;
border-radius: 3px; border-radius: 3px;
} }
@ -33,8 +34,8 @@
.inputTime{ .inputTime{
width: fit-content; width: fit-content;
border: 3px solid #007AFF26; border: 3px solid #007AFF26;
padding: 12px 22px; padding: 8px 25px;
font-size: 20px; font-size: 15px;
font-weight: 400; font-weight: 400;
border-radius: 3px; border-radius: 3px;
} }

View file

@ -1,6 +1,7 @@
/*TypeDropdown.module.css*/ /*TypeDropdown.module.css*/
.dropdownContainer { .dropdownContainer {
margin-top: -5px;
width: 23%; width: 23%;
position: relative; position: relative;
display: inline-block; display: inline-block;
@ -12,7 +13,7 @@
border: 1px solid #000000; border: 1px solid #000000;
border-radius: 19px; border-radius: 19px;
padding: 9px 7px 7px 10px; padding: 9px 7px 7px 10px;
font-size: 16px; font-size: 13px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -25,7 +26,6 @@
.selectedTypeIcon { .selectedTypeIcon {
margin-right: 4px; margin-right: 4px;
width: 22px;
} }
.dropdownArrow { .dropdownArrow {
@ -33,7 +33,7 @@
} }
.dropdownList { .dropdownList {
width: 70%; width: 85%;
margin-top: 11px; margin-top: 11px;
position: absolute; position: absolute;
background-color: #fff; background-color: #fff;
@ -58,13 +58,11 @@
.dropdownItemIcon { .dropdownItemIcon {
margin-right: 5px; margin-right: 5px;
width: 24px;
} }
.selectedTypeIcon, .selectedTypeIcon,
.dropdownItemIcon { .dropdownItemIcon {
width: 20px; width: 17px;
height: 20px;
margin-right: 5px; margin-right: 5px;
vertical-align: middle; vertical-align: middle;
} }

View file

@ -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<SurveyContextType | undefined>(undefined);
export const SurveyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [tempSurvey, setTempSurvey] = useState<INewSurvey & { questions: Question[] }>({
title: "",
description: "",
questions: [],
});
const clearTempSurvey = () => {
setTempSurvey({ title: "", description: "", questions: [] });
};
return (
<SurveyContext.Provider value={{ tempSurvey, setTempSurvey, clearTempSurvey }}>
{children}
</SurveyContext.Provider>
);
};
export const useSurveyContext = () => {
const context = useContext(SurveyContext);
if (!context) {
throw new Error("useSurveyContext must be used within a SurveyProvider");
}
return context;
};

View file

@ -0,0 +1,6 @@
import { useLocation } from 'react-router-dom';
export const useRouteReadOnly = () => {
const location = useLocation();
return location.pathname.includes('/complete-survey/');
};

View file

@ -2,11 +2,14 @@
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
background-color: #F6F6F6; background-color: #F6F6F6;
padding: 61.5px 0; padding: 50px 0;
} }
.pageLogin{ .pageLogin{
min-height: 80vh;
width: 100%; width: 100%;
background-color: #F6F6F6; background-color: #F6F6F6;
padding: 157px 0; padding: 100px 0;
display: flex;
justify-content: center;
} }

View file

@ -2,6 +2,7 @@ import styles from './AuthForm.module.css';
import LoginForm from "../../components/LoginForm/LoginForm.tsx"; import LoginForm from "../../components/LoginForm/LoginForm.tsx";
import RegisterForm from "../../components/RegisterForm/RegisterForm.tsx"; import RegisterForm from "../../components/RegisterForm/RegisterForm.tsx";
import {useLocation} from "react-router-dom"; import {useLocation} from "react-router-dom";
import {useEffect} from "react";
const AuthForm = () => { const AuthForm = () => {
@ -9,6 +10,22 @@ const AuthForm = () => {
const isLoginPage = location.pathname === '/login'; const isLoginPage = location.pathname === '/login';
const isRegisterPage = location.pathname === '/register'; 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; let content;
if (isLoginPage) { if (isLoginPage) {
content = <LoginForm />; content = <LoginForm />;

View file

@ -1,11 +1,9 @@
import Header from "../../components/Header/Header.tsx";
import styles from './CompleteSurvey.module.css' import styles from './CompleteSurvey.module.css'
import CompletingSurvey from "../../components/CompletingSurvey/CompletingSurvey.tsx"; import CompletingSurvey from "../../components/CompletingSurvey/CompletingSurvey.tsx";
export const CompleteSurvey = () => { export const CompleteSurvey = () => {
return( return(
<div className={styles.layout}> <div className={styles.layout}>
<Header/>
<CompletingSurvey/> <CompletingSurvey/>
</div> </div>
) )