diff --git a/SurveyFrontend/package-lock.json b/SurveyFrontend/package-lock.json index 432a9d5..84dc8dc 100644 --- a/SurveyFrontend/package-lock.json +++ b/SurveyFrontend/package-lock.json @@ -9,9 +9,13 @@ "version": "0.0.0", "dependencies": { "@formkit/tempo": "^0.1.2", + "chart.js": "^4.4.9", + "chartjs-plugin-annotation": "^3.1.0", + "chartjs-plugin-datalabels": "^2.2.0", "mobx": "^6.13.7", "mobx-react": "^9.2.0", "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.2", "react-textarea-autosize": "^8.5.9", @@ -972,6 +976,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1989,6 +1999,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=4.0.0" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3102,6 +3142,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", diff --git a/SurveyFrontend/package.json b/SurveyFrontend/package.json index 79c2b12..429ea89 100644 --- a/SurveyFrontend/package.json +++ b/SurveyFrontend/package.json @@ -11,9 +11,13 @@ }, "dependencies": { "@formkit/tempo": "^0.1.2", + "chart.js": "^4.4.9", + "chartjs-plugin-annotation": "^3.1.0", + "chartjs-plugin-datalabels": "^2.2.0", "mobx": "^6.13.7", "mobx-react": "^9.2.0", "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.2", "react-textarea-autosize": "^8.5.9", diff --git a/SurveyFrontend/src/App.tsx b/SurveyFrontend/src/App.tsx index 8838013..3f02788 100644 --- a/SurveyFrontend/src/App.tsx +++ b/SurveyFrontend/src/App.tsx @@ -8,6 +8,8 @@ import {Results} from "./components/Results/Results.tsx"; import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx"; import AuthForm from "./pages/AuthForm/AuthForm.tsx"; import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx"; +import CompleteSurvey from "./pages/CompleteSurvey/CompleteSurvey.tsx"; +import CompletingSurvey from "./components/CompletingSurvey/CompletingSurvey.tsx"; const App = () => { return( @@ -31,6 +33,10 @@ const App = () => { } /> + }> + }/> + + } /> diff --git a/SurveyFrontend/src/api/AnswerApi.ts b/SurveyFrontend/src/api/AnswerApi.ts new file mode 100644 index 0000000..f29d5b4 --- /dev/null +++ b/SurveyFrontend/src/api/AnswerApi.ts @@ -0,0 +1,94 @@ +import {BASE_URL, createRequestConfig, handleResponse} 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) => { + 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 responseData = await handleResponse(response); + if (response.ok && !responseData){ + return {success: true}; + } + return responseData; + } + catch(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 4b0b45b..f549b25 100644 --- a/SurveyFrontend/src/api/AuthApi.ts +++ b/SurveyFrontend/src/api/AuthApi.ts @@ -10,16 +10,10 @@ interface IRegistrationData extends IAuthData{ firstName: string; lastName: string; } -// -// interface IUserData{ -// username: string; -// firstName: string; -// lastName: string; -// email: string; -// } -export const getCurrentUser = async (): Promise => { +export const getCurrentUser = async () => { const token = localStorage.getItem("token"); + if (!token) { throw new Error("Токен отсутствует"); } @@ -32,6 +26,8 @@ export const getCurrentUser = async (): Promise => { } }); + console.log(response); + if (response.status === 401) { localStorage.removeItem("token"); throw new Error("Сессия истекла. Пожалуйста, войдите снова."); diff --git a/SurveyFrontend/src/api/BaseApi.ts b/SurveyFrontend/src/api/BaseApi.ts index 2041e4f..6a85675 100644 --- a/SurveyFrontend/src/api/BaseApi.ts +++ b/SurveyFrontend/src/api/BaseApi.ts @@ -20,12 +20,10 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque headers: {}, }; - // Добавляем заголовок авторизации, если есть токен if (token) { config.headers.Authorization = `Bearer ${token}`; } - // Добавляем Content-Type, если это не FormData if (!isFormData) { config.headers["Content-Type"] = "application/json"; } @@ -39,12 +37,16 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque * @returns Распарсенные данные или ошибку */ const handleResponse = async (response: Response) => { - // Проверяем, есть ли контент в ответе const responseText = await response.text(); if (!responseText) { + if (response.status === 401) { + window.location.href = '/auth/login'; + throw new Error('Требуется авторизация'); + } + if (response.ok) { - return null; // Если ответ пустой, но статус 200, возвращаем null + return null; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } diff --git a/SurveyFrontend/src/api/QuestionApi.ts b/SurveyFrontend/src/api/QuestionApi.ts index 0d80838..90593b6 100644 --- a/SurveyFrontend/src/api/QuestionApi.ts +++ b/SurveyFrontend/src/api/QuestionApi.ts @@ -1,4 +1,5 @@ import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts"; +import {IAnswerVariant} from "./AnswerApi.ts"; export interface INewQuestion{ title: string; @@ -8,11 +9,7 @@ export interface INewQuestion{ export interface IQuestion extends INewQuestion { id: number; surveyId: number; - answerVariants: Array<{ - id: number; - questionId: number; - text: string; - }> + answerVariants: IAnswerVariant[]; } export const addNewQuestion = async (surveyId: number, question: INewQuestion) => { diff --git a/SurveyFrontend/src/api/SurveyApi.ts b/SurveyFrontend/src/api/SurveyApi.ts index bec3f70..4c20c73 100644 --- a/SurveyFrontend/src/api/SurveyApi.ts +++ b/SurveyFrontend/src/api/SurveyApi.ts @@ -5,6 +5,7 @@ export interface ISurvey { title: string; description: string; createdBy: number; + createdAt: string; } export interface INewSurvey{ @@ -51,30 +52,6 @@ export const getAllSurveys = async (): Promise => { * postNewSurvey - добавление нового опроса * @param survey */ -// export const postNewSurvey = async (survey: INewSurvey): Promise => { -// const token = localStorage.getItem("token"); -// if (!token) { -// throw new Error("Токен отсутствует"); -// } -// -// try{ -// const response = await fetch(`${BASE_URL}/surveys`, { -// ...createRequestConfig('POST'), -// body: JSON.stringify(survey) -// }) -// -// // return await handleResponse(response); -// -// if (response.status === 200) { -// return await handleResponse(response); -// } -// throw new Error(`Ожидался код 200, получен ${response.status}`); -// } catch (error) { -// console.error(`Error when adding a new survey: ${error}`); -// throw error; -// } -// } - export const postNewSurvey = async (survey: INewSurvey): Promise => { const token = localStorage.getItem("token"); if (!token) { diff --git a/SurveyFrontend/src/assets/gmail_groups.svg b/SurveyFrontend/src/assets/gmail_groups.svg new file mode 100644 index 0000000..3e4293d --- /dev/null +++ b/SurveyFrontend/src/assets/gmail_groups.svg @@ -0,0 +1,3 @@ + + + diff --git a/SurveyFrontend/src/assets/send.svg b/SurveyFrontend/src/assets/send.svg new file mode 100644 index 0000000..6a54f6e --- /dev/null +++ b/SurveyFrontend/src/assets/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx b/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx index 639d027..1bf5bb5 100644 --- a/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx +++ b/SurveyFrontend/src/components/AnswerOption/AnswerOption.tsx @@ -3,17 +3,22 @@ import styles from'./AnswerOption.module.css'; import Delete from '../../assets/delete.svg?react'; import Single from '../../assets/radio_button_unchecked.svg?react'; 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'; interface AnswerOptionProps{ index: number; value: string; - onChange: (value: string) => void; - onDelete:(index: number) => void; - selectedType: 'single' | 'multiply'; - toggleSelect: () => void; + onChange?: (value: string) => void; + onDelete?:(index: number) => void; + selectedType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion'; + isSelected?: boolean; + toggleSelect?: () => void; + isCompleteSurveyActive?: boolean; } -const AnswerOption: React.FC = ({index, value, onChange, onDelete, selectedType, toggleSelect}) => { +const AnswerOption: React.FC = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect, isCompleteSurveyActive = false}) => { const [currentValue, setCurrentValue] = useState(value); const [isEditing, setIsEditing] = useState(false); @@ -29,7 +34,6 @@ const AnswerOption: React.FC = ({index, value, onChange, onDe const handleTextareaChange = (event: React.ChangeEvent) => { setCurrentValue(event.target.value); - // Автоматическое изменение высоты if (textAreaRef.current) { textAreaRef.current.style.height = 'auto'; textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; @@ -39,7 +43,6 @@ const AnswerOption: React.FC = ({index, value, onChange, onDe useEffect(() => { if (isEditing && textAreaRef.current) { textAreaRef.current.focus(); - // Установка начальной высоты textAreaRef.current.style.height = 'auto'; textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; } @@ -47,7 +50,7 @@ const AnswerOption: React.FC = ({index, value, onChange, onDe const handleSave = () => { setIsEditing(false); - onChange(currentValue); + onChange?.(currentValue); }; const handleKeyDown = (event: React.KeyboardEvent) => { @@ -67,16 +70,50 @@ const AnswerOption: React.FC = ({index, value, onChange, onDe } }, [isEditing]); + const handleMarkerClick = () => { + if (isCompleteSurveyActive && toggleSelect) { + toggleSelect(); + } + }; + + return (
- - {isEditing ? ( -