Merge branch 'correctionComponent' into 'unstable'

Correction component

See merge request internship-2025/survey-webapp/survey-webapp!11
This commit is contained in:
Tatyana Nikolaeva 2025-04-28 07:20:45 +00:00
commit d7734cb68a
31 changed files with 483 additions and 195 deletions

View file

@ -11,6 +11,7 @@
"@formkit/tempo": "^0.1.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.2",
"uuid": "^11.1.0"
},
"devDependencies": {
@ -1731,6 +1732,15 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2626,6 +2636,45 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
"integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.2.tgz",
"integrity": "sha512-yk1XW8Fj7gK7flpYBXF3yzd2NbX6P7Kxjvs2b5nu1M04rb5pg/Zc4fGdBNTeT4eDYL2bvzWNyKaIMJX/RKHTTg==",
"license": "MIT",
"dependencies": {
"react-router": "7.5.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -2720,6 +2769,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2798,6 +2853,12 @@
"typescript": ">=4.8.4"
}
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View file

@ -13,6 +13,7 @@
"@formkit/tempo": "^0.1.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.2",
"uuid": "^11.1.0"
},
"devDependencies": {

View file

@ -1,11 +1,35 @@
import React from 'react';
import './App.css'
import Questions from './pages/Questions.tsx'
import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
import {SurveyCreateAndEditingPage} from "./pages/SurveyCreateAndEditingPage/SurveyCreateAndEditingPage.tsx";
import Survey from "./components/Survey/Survey.tsx";
import SettingSurvey from "./components/SettingSurvey/SettingSurvey.tsx";
import {MySurveysPage} from "./pages/MySurveysPage/MySurveysPage.tsx";
import {Results} from "./components/Results/Results.tsx";
import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx";
const App: React.FC = () => {
return (
<Questions />
)
const App = () => {
return(
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/survey/create/questions" replace />} />
<Route path="survey/create" element={<SurveyCreateAndEditingPage />}>
<Route path="questions" element={<Survey />} />
<Route path="settings" element={<SettingSurvey />} />
</Route>
<Route path="my-surveys" element={<MySurveysPage />}>
<Route index element={<MySurveyList />} />
</Route>
<Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}>
<Route path="questions" element={<Survey />} />
<Route path="settings" element={<SettingSurvey />} />
<Route path="results" element={<Results />} />
</Route>
</Routes>
</BrowserRouter>
);
}

View file

@ -0,0 +1,37 @@
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts";
interface IAuthData{
email: string;
password: string;
}
interface IRegistrationData extends IAuthData{
username: string;
firstName: string;
lastName: string;
}
export const registerUser = async (data: IRegistrationData) => {
try{
const response = await fetch(`${BASE_URL}/auth/register`, {
...createRequestConfig('POST'), body: JSON.stringify(data),
})
return await handleResponse(response);
} catch (error){
console.error("Registration error:", error);
throw error;
}
}
export const authUser = async (data: IAuthData) => {
try{
const response = await fetch(`${BASE_URL}/auth/login`, {
...createRequestConfig('POST'), body: JSON.stringify(data),
})
return await handleResponse(response);
}
catch(error){
console.error("Login error:", error);
throw error;
}
}

View file

@ -0,0 +1,50 @@
const BASE_URL = "https://survey.slavagm.ru/api";
interface RequestConfig {
method: string;
headers: Record<string, string>;
body?: BodyInit | null;
}
/**
* Создаёт конфигурацию для fetch-запроса
* @param method HTTP-метод (GET, POST, PUT, DELETE)
* @param isFormData Флаг, указывающий, что отправляется FormData
* @returns Конфигурация для fetch-запроса
*/
const createRequestConfig = (method: string, isFormData: boolean = false): RequestConfig => {
const token = localStorage.getItem("accessToken");
const config: RequestConfig = {
method,
headers: {},
};
// Добавляем заголовок авторизации, если есть токен
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Добавляем Content-Type, если это не FormData
if (!isFormData) {
config.headers["Content-Type"] = "application/json";
}
return config;
};
/**
* Обрабатывает ответ от сервера
* @param response Ответ от fetch
* @returns Распарсенные данные или ошибку
*/
const handleResponse = async (response: Response) => {
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Произошла ошибка");
}
return data;
};
export { BASE_URL, createRequestConfig, handleResponse };

View file

@ -7,18 +7,27 @@
padding: 4.58px 13px 4.58px 4.58px;
margin: 26px 33px 27px 0;
margin-left: auto;
display: inline-flex;
max-width: 100%;
min-width: fit-content;
}
.accountText{
width: 100%;
gap: 9px;
display: flex;
align-items: center;
justify-content: space-around;
font-size: 24px;
font-weight: 600;
color: black;
width: 100%;
text-decoration: none;
white-space: nowrap;
}
.accountImg{
vertical-align: middle;
width: 55px;
margin-right: 9px;
}
flex-shrink: 0;
}

View file

@ -1,7 +1,6 @@
/*AddQuestionButton.module.css*/
.questionButton{
display: block;
margin: 0 auto;
display: flex;
flex-direction: column;

View file

@ -5,4 +5,39 @@
padding: 0;
width: 100%;
display: flex;
}
.pagesNav{
display: flex;
gap: 60px;
list-style: none;
align-items: center;
margin-right: 40%;
}
.pageLink{
font-size: 24px;
font-weight: 600;
color: #2A6DAE;
padding: 0;
border: none;
background-color: #ffffff;
white-space: nowrap;
text-decoration: none;
}
.active{
margin-bottom: -15px;
color: #000000;
text-decoration-color: #3881C8;
}
.activeLine{
display: block;
margin-top: 5px;
border: 1px solid #000000;
width: 50%;
padding: 0;
box-shadow: 0 1px 4px 0 #3881C8;
}

View file

@ -1,29 +1,38 @@
import React, {useState} from "react";
import React from "react";
import Logo from "../Logo/Logo.tsx";
import Account from "../Account/Account.tsx";
import styles from './Header.module.css'
import SurveyPagesList from "../SurveyPagesList/SurveyPagesList.tsx";
import {Link, useLocation} from "react-router-dom";
const Header: React.FC = () => {
const [activePage, setActivePage] = useState('Создать опрос');
const handlePageClick = (name: string)=> {
setActivePage(name);
}
const location = useLocation();
const isCreateSurveyActive = location.pathname.includes('/survey/create');
const isSurveyPage = location.pathname.includes('/survey/') && !location.pathname.includes('/survey/create');
const isMySurveysPage = location.pathname === '/my-surveys' || isSurveyPage;
return (
<div className={styles.header}>
<Logo href='' />
<SurveyPagesList
activePage={activePage}
onPageClick = {handlePageClick}
/>
<Logo href='/' />
<nav className={styles.pagesNav}>
<Link to='/survey/create/questions'
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}>
Создать опрос
{isCreateSurveyActive && <hr className={styles.activeLine}/>}
</Link>
<Link to='/my-surveys'
className={`${styles.pageLink} ${isMySurveysPage ? styles.active : ''}`}>
Мои опросы
{isMySurveysPage && <hr className={styles.activeLine}/>}
</Link>
</nav>
<Account
href=''
href='/profile'
user='Иванов Иван'
/>
</div>
);
};
export default Header;
export default Header;

View file

@ -1,8 +0,0 @@
/*MainComponent.module.css*/
.mainPage{
width: 100%;
min-height: 85vh;
display: flex;
background-color: #F6F6F6;
}

View file

@ -1,29 +0,0 @@
import Navigation from "../Navigation/Navigation.tsx";
import React, {useState} from "react";
import styles from './MainComponent.module.css'
import Survey from "../Survey/Survey.tsx";
import SettingSurvey from "../SettingSurvey/SettingSurvey.tsx";
const MainComponent: React.FC = () => {
const [activePage, setActivePage] = useState(
localStorage.getItem("activePage") || "Вопросы"
);
const handleNavigationClick = (title: string) => {
setActivePage(title);
localStorage.setItem('activePage', title);
}
return (
<main className={styles.mainPage}>
<Navigation
activePage={activePage}
onNavigationClick={handleNavigationClick}
/>
{ activePage === 'Вопросы' && <Survey />}
{activePage === 'Настройки' && <SettingSurvey />}
</main>
)
}
export default MainComponent;

View file

@ -0,0 +1,60 @@
import styles from './MySurveysList.module.css'
import {useNavigate} from "react-router-dom";
interface MySurveyItem{
id: string,
title: string,
description: string,
date: string
status: 'active' | 'completed'
}
export const MySurveyList = () => {
const navigate = useNavigate();
const surveys: MySurveyItem[] = [
{
id: '1',
title: 'Опрос 1',
description: 'Описание опроса 1',
date: '27-04-2025',
status: 'active',
},
{
id: '2',
title: 'Опрос 2',
description: 'Описание опроса 2',
date: '01-01-2025',
status: 'completed',
}
]
const handleSurveyClick = (id: string) => {
navigate(`/survey/${id}/questions`)
}
return(
<div className={styles.main}>
{surveys.map((survey) => (
<button
key={survey.id}
className={styles.survey}
onClick={() => handleSurveyClick(survey.id)}
>
<div className={styles.textContent}>
<div className={styles.surveyData}>
<h1 className={styles.title}>{survey.title}</h1>
<h2 className={styles.description}>{survey.description}</h2>
</div>
<span className={styles.date}>Дата создания: {survey.date}</span>
</div>
<div className={`${styles.status} ${
survey.status === 'active' ? styles.active : styles.completed
}`}>
{survey.status === 'active' ? 'Активен' : 'Завершён'}
</div>
</button>
))}
</div>
)
}

View file

@ -0,0 +1,67 @@
.main {
background-color: #F6F6F6;
width: 100%;
min-height: 100vh;
padding: 34px 10%;
}
.survey {
display: flex;
justify-content: space-between;
background-color: white;
width: 79%;
border-radius: 14px;
padding: 29px 36px 29px 54px;
margin-bottom: 23px;
gap: 20px;
border: none;
text-align: left;
font: inherit;
outline: none;
}
.textContent {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.status {
width: fit-content;
height: fit-content;
padding: 15px 47px;
border-radius: 15px;
color: #FFFFFF;
white-space: nowrap;
margin-left: 20px;
}
.completed {
background-color: #B0B0B0;
}
.active {
background-color: #65B953;
}
.surveyData {
margin-bottom: 33px;
}
.title {
font-size: 40px;
font-weight: 600;
word-break: break-word;
}
.description {
font-size: 24px;
font-weight: 500;
word-break: break-word;
}
.date {
font-size: 18px;
font-weight: 500;
color: #7D7983;
}

View file

@ -1,15 +1,31 @@
import React from 'react'
import {useLocation, useNavigate} from 'react-router-dom'
import styles from './Navigation.module.css'
import NavigationItem from "../NavigationItem/NavigationItem.tsx";
import SaveButton from "../SaveButton/SaveButton.tsx";
interface NavigationProps {
onNavigationClick: (title: string) => void;
activePage: string
}
const Navigation: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const Navigation: React.FC<NavigationProps> = ({onNavigationClick, activePage}) => {
const items: string[] = ['Вопросы', 'Настройки', 'Результаты']
const isSurveyPage = /\/survey\/[^/]+/.test(location.pathname);
const isNotCreateSurvey = !location.pathname.includes('/survey/create');
const isMySurveysPage = isSurveyPage && isNotCreateSurvey;
const activePage = location.pathname.split('/').pop() ?? 'questions';
const baseItems = [
{id: 'questions', title: 'Вопросы'},
{id: 'settings', title: 'Настройки'}
];
const items = isMySurveysPage
? [...baseItems, {id: 'results', title: 'Результаты'}]
: baseItems;
const handleNavigationClick = (pageId: string) => {
navigate(`${pageId}`, { relative: 'path' });
};
return (
<div className={styles.navContainer}>
@ -17,15 +33,14 @@ const Navigation: React.FC<NavigationProps> = ({onNavigationClick, activePage})
<ul className={styles.navList}>
{items.map(item => (
<NavigationItem
key={item}
title={item}
isActive={activePage === item}
onClick={() => onNavigationClick(item)}
key={item.id}
title={item.title}
isActive={activePage === item.id}
onClick={() => handleNavigationClick(item.id)}
/>
))}
</ul>
</nav>
<SaveButton />
</div>
);

View file

@ -1,22 +0,0 @@
/*PageSurvey.module.css*/
.pagesSurveyItem{
align-items: center;
}
.pageSurvey{
font-size: 24px;
font-weight: 600;
color: #2A6DAE;
padding: 0;
border: none;
background-color: #ffffff;
padding-bottom: 5px;
white-space: nowrap;
}
.active{
color: #000000;
text-decoration: underline;
text-decoration-color: #3881C8;
}

View file

@ -1,20 +0,0 @@
import React from 'react';
import styles from './PageSurvey.module.css';
interface PageSurveyProps{
name: string;
isActive: boolean;
onClick(): void;
}
const PageSurvey: React.FC<PageSurveyProps> = ({name, isActive, onClick}) => {
return (
<li className={styles.pagesSurveyItem}>
<button className={`${styles.pageSurvey} ${isActive ? styles.active : ''}`} onClick={onClick}>
{name}
</button>
</li>
);
};
export default PageSurvey;

View file

@ -0,0 +1,5 @@
/*Results.module.css*/
.results{
width: 85%;
}

View file

@ -0,0 +1,10 @@
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import styles from './Results.module.css'
export const Results = () => {
return(
<div className={styles.results}>
<SurveyInfo />
</div>
)
}

View file

@ -1,8 +1,7 @@
/*SettingSurvey.module.css*/
.settingSurvey{
width: 65%;
margin-left: 8.9%;
width: 85%;
}
.startEndTime{

View file

@ -1,6 +1,5 @@
/*Survey.module.css*/
.survey{
width: 65%;
margin-left: 8.9%;
width: 85%;
}

View file

@ -13,7 +13,7 @@
.info{
min-width: 373px;
display: block;
padding: 35px; /*подумать нужно ли справа слева отступы*/
padding: 35px;
}
.titleSurvey{

View file

@ -1,9 +0,0 @@
/*SurveyPagesList.module.css*/
.listSurveyPages{
display: flex;
gap: 61px;
list-style: none;
align-items: center;
margin-right: 40%;
}

View file

@ -1,27 +0,0 @@
import React from 'react';
import styles from './SurveyPagesList.module.css';
import PageSurvey from "../PageSurvey/PageSurvey.tsx";
interface SurveyPagesListProps{
activePage: string;
onPageClick: (name: string) => void;
}
const SurveyPagesList: React.FC<SurveyPagesListProps> = ({activePage, onPageClick}) => {
const listPages: string[] = ['Создать опрос', 'Мои опросы']
return (
<ul className={styles.listSurveyPages}>
{listPages.map((page) => (
<PageSurvey
key={page}
name={page}
isActive={activePage === page}
onClick={() => onPageClick(page)}
/>
))}
</ul>
);
};
export default SurveyPagesList;

View file

@ -4,7 +4,6 @@
width: 23%;
position: relative;
display: inline-block;
/*margin-right: 29px;*/
margin-left: auto;
}

View file

@ -0,0 +1,17 @@
/*MySurveysPage.module.css*/
.layout{
width: 100%;
}
.main{
width: 100%;
min-height: 85vh;
display: flex;
background-color: #F6F6F6;
}
.content{
width: 100%;
margin-left: 8.9%;
}

View file

@ -0,0 +1,12 @@
import styles from "./MySurveysPage.module.css";
import {MySurveyList} from "../../components/MySurveyList/MySurveyList.tsx";
import Header from "../../components/Header/Header.tsx";
export const MySurveysPage = () => {
return (
<div className={styles.layout}>
<Header />
<MySurveyList />
</div>
)
}

View file

@ -1,14 +0,0 @@
import React from 'react';
import Header from '../components/Header/Header.tsx'
import MainComponent from "../components/MainComponent/MainComponent.tsx";
const QuestionsPages: React.FC = () => {
return (
<>
<Header />
<MainComponent />
</>
)
}
export default QuestionsPages;

View file

@ -1,11 +0,0 @@
import React from 'react';
import Header from "../components/Header/Header.tsx";
const Results: React.FC = () => {
return (
<Header />
)
}
export default Results;

View file

@ -1,15 +0,0 @@
import React from 'react';
import Header from "../components/Header/Header.tsx";
import MainComponent from "../components/MainComponent/MainComponent.tsx";
const Settings: React.FC = () => {
return (
<>
<Header />
<MainComponent />
</>
);
};
export default Settings;

View file

@ -0,0 +1,17 @@
/*SurveyCreateAndEditingPage.module.css*/
.layout{
width: 100%;
}
.main{
width: 100%;
min-height: 85vh;
display: flex;
background-color: #F6F6F6;
}
.content{
width: 100%;
margin-left: 8.9%;
}

View file

@ -0,0 +1,18 @@
import Header from "../../components/Header/Header.tsx";
import Navigation from "../../components/Navigation/Navigation.tsx";
import styles from './SurveyCreateAndEditingPage.module.css'
import { Outlet } from "react-router-dom";
export const SurveyCreateAndEditingPage = () => {
return (
<div className={styles.layout}>
<Header />
<div className={styles.main}>
<Navigation />
<div className={styles.content}>
<Outlet />
</div>
</div>
</div>
);
};