Merge branch 'creating_components' into 'unstable'

Change components and styles

See merge request internship-2025/survey-webapp/survey-webapp!2
This commit is contained in:
Tatyana Nikolaeva 2025-04-05 18:17:37 +00:00
commit 74dd1905c2
45 changed files with 486 additions and 345 deletions

View file

@ -2,9 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Survey</title>
</head>
<body>
<div id="root"></div>

View file

@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@ -2876,6 +2877,19 @@
"punycode": "^2.1.0"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",

View file

@ -11,7 +11,8 @@
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",

View file

@ -0,0 +1,4 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.1579 1.5V14.5" stroke="#3788D6" stroke-width="2" stroke-linecap="round"/>
<path d="M14 8L1 8" stroke="#3788D6" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View file

@ -0,0 +1,21 @@
<svg width="91" height="90" viewBox="0 0 91 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3_6)">
<circle cx="45.5" cy="45" r="27" fill="#3788D6"/>
</g>
<path d="M45.5 26V64" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M65.5 45L27.5 45" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M45.5 26V64" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M65.5 45L27.5 45" stroke="white" stroke-width="2" stroke-linecap="round"/>
<defs>
<filter id="filter0_d_3_6" x="0.6" y="0.1" width="89.8" height="89.8" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="8.95"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.310533 0 0 0 0 0.630263 0 0 0 0 0.938151 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3_6"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3_6" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15L7 10H17L12 15Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 14L12 9L17 14H7Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 150 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.06667 10.8L11.7667 6.1L10.8333 5.16667L7.06667 8.93333L5.16667 7.03333L4.23333 7.96667L7.06667 10.8ZM3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667V3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13056 2.96667 2 3.33333 2H12.6667C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333V12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14H3.33333Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 11.3333C8.92226 11.3333 9.70837 11.0083 10.3584 10.3583C11.0084 9.70834 11.3334 8.92223 11.3334 8.00001C11.3334 7.07779 11.0084 6.29168 10.3584 5.64168C9.70837 4.99168 8.92226 4.66668 8.00004 4.66668C7.07782 4.66668 6.29171 4.99168 5.64171 5.64168C4.99171 6.29168 4.66671 7.07779 4.66671 8.00001C4.66671 8.92223 4.99171 9.70834 5.64171 10.3583C6.29171 11.0083 7.07782 11.3333 8.00004 11.3333ZM8.00004 14.6667C7.07782 14.6667 6.21115 14.4917 5.40004 14.1417C4.58893 13.7917 3.88337 13.3167 3.28337 12.7167C2.68337 12.1167 2.20837 11.4111 1.85837 10.6C1.50837 9.7889 1.33337 8.92223 1.33337 8.00001C1.33337 7.07779 1.50837 6.21112 1.85837 5.40001C2.20837 4.5889 2.68337 3.88334 3.28337 3.28334C3.88337 2.68334 4.58893 2.20834 5.40004 1.85834C6.21115 1.50834 7.07782 1.33334 8.00004 1.33334C8.92226 1.33334 9.78893 1.50834 10.6 1.85834C11.4112 2.20834 12.1167 2.68334 12.7167 3.28334C13.3167 3.88334 13.7917 4.5889 14.1417 5.40001C14.4917 6.21112 14.6667 7.07779 14.6667 8.00001C14.6667 8.92223 14.4917 9.7889 14.1417 10.6C13.7917 11.4111 13.3167 12.1167 12.7167 12.7167C12.1167 13.3167 11.4112 13.7917 10.6 14.1417C9.78893 14.4917 8.92226 14.6667 8.00004 14.6667ZM8.00004 13.3333C9.48893 13.3333 10.75 12.8167 11.7834 11.7833C12.8167 10.75 13.3334 9.4889 13.3334 8.00001C13.3334 6.51112 12.8167 5.25001 11.7834 4.21668C10.75 3.18334 9.48893 2.66668 8.00004 2.66668C6.51115 2.66668 5.25004 3.18334 4.21671 4.21668C3.18337 5.25001 2.66671 6.51112 2.66671 8.00001C2.66671 9.4889 3.18337 10.75 4.21671 11.7833C5.25004 12.8167 6.51115 13.3333 8.00004 13.3333Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99999 18.3333C8.84721 18.3333 7.76388 18.1146 6.74999 17.6771C5.7361 17.2396 4.85416 16.6458 4.10416 15.8958C3.35416 15.1458 2.76041 14.2639 2.32291 13.25C1.88541 12.2361 1.66666 11.1528 1.66666 9.99999C1.66666 8.84721 1.88541 7.76388 2.32291 6.74999C2.76041 5.7361 3.35416 4.85416 4.10416 4.10416C4.85416 3.35416 5.7361 2.76041 6.74999 2.32291C7.76388 1.88541 8.84721 1.66666 9.99999 1.66666C11.1528 1.66666 12.2361 1.88541 13.25 2.32291C14.2639 2.76041 15.1458 3.35416 15.8958 4.10416C16.6458 4.85416 17.2396 5.7361 17.6771 6.74999C18.1146 7.76388 18.3333 8.84721 18.3333 9.99999C18.3333 11.1528 18.1146 12.2361 17.6771 13.25C17.2396 14.2639 16.6458 15.1458 15.8958 15.8958C15.1458 16.6458 14.2639 17.2396 13.25 17.6771C12.2361 18.1146 11.1528 18.3333 9.99999 18.3333ZM9.99999 16.6667C11.8611 16.6667 13.4375 16.0208 14.7292 14.7292C16.0208 13.4375 16.6667 11.8611 16.6667 9.99999C16.6667 8.13888 16.0208 6.56249 14.7292 5.27082C13.4375 3.97916 11.8611 3.33332 9.99999 3.33332C8.13888 3.33332 6.56249 3.97916 5.27082 5.27082C3.97916 6.56249 3.33332 8.13888 3.33332 9.99999C3.33332 11.8611 3.97916 13.4375 5.27082 14.7292C6.56249 16.0208 8.13888 16.6667 9.99999 16.6667Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,42 +1,3 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
width: 100%;
}

View file

@ -1,35 +1,12 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import React from 'react';
import './App.css'
import Questions from './pages/Questions.tsx'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
const App: React.FC = () => {
return (
<Questions />
)
}
export default App

View file

@ -1,9 +1,24 @@
/*Account.module.css*/
.account {
margin: 0;
padding: 0;
width: 52px;
height: 52px;
margin: 31px 39px 25px 0;
width: 15%;
background-color: #F3F3F3;
border-radius: 40px;
align-items: center;
padding: 4.58px 13px 4.58px 4.58px;
margin: 26px 33px 27px 0;
}
.accountText{
font-size: 24px;
font-weight: 600;
color: black;
width: 100%;
text-decoration: none;
}
.accountImg{
vertical-align: middle;
width: 55px;
margin-right: 9px;
}

View file

@ -2,14 +2,18 @@ import React from 'react';
import styles from './Account.module.css'
interface AccountProps {
href: string
href: string;
user: string;
}
const Account: React.FC<AccountProps> = ({href}) => {
const Account: React.FC<AccountProps> = ({href, user}) => {
return (
<a className={styles.account} href={href}>
<img src='../../../public/account.svg' alt='Личный кабинет'/>
</a>
<div className={styles.account}>
<a className={styles.accountText} href={href}>
<img src='../../../public/account.svg' className={styles.accountImg} alt='account'/>
{user}
</a>
</div>
);
};

View file

@ -1,6 +1,7 @@
/*AddAnswerButton.module.css*/
.answerButton {
margin-top: 18px;
display: flex;
gap: 10px;
align-items: center;

View file

@ -1,6 +1,7 @@
import React from "react";
import styles from './AddAnswerButton.module.css'
interface AddAnswerButtonProps {
onClick(): void;
}
@ -8,8 +9,8 @@ interface AddAnswerButtonProps {
const AddAnswerButton: React.FC<AddAnswerButtonProps> = ({onClick}) => {
return (
<button className={styles.answerButton} onClick={onClick}>
Добавить вариант ответа
<img className={styles.addAnswerImg} src='../../../public/add_answer.svg' alt=''/>
Добавить вариант ответа {' '}
<img src='../../../public/add_answer.svg' className={styles.addAnswerImg} alt="add answer"/>
</button>
);
};

View file

@ -1,18 +1,24 @@
/*AddQuestionButton.module.css*/
.questionButton{
background-color: #F6F6F6;
font-size: 24px;
font-weight: 600;
border: none;
margin: 104px auto 80px;
display: block;
margin: 0 auto;
display: flex;
flex-direction: column;
margin-bottom: 80px;
align-items: center;
background-color: #F6F6F6;
border: none;
outline: none;
}
.questionButtonImg{
vertical-align: middle;
margin: 0 auto;
margin-bottom: 16px;
width: 54px;
align-items: center;
}
.textButton{
font-size: 24px;
font-weight: 600;
text-align: center;
}

View file

@ -8,8 +8,8 @@ interface AddQuestionButtonProps {
const AddQuestionButton: React.FC<AddQuestionButtonProps> = ({onClick}) => {
return (
<button className={styles.questionButton} onClick={onClick}>
<img className={styles.questionButtonImg} src='../../../public/add_question.svg' alt=''/>
Добавить вопрос
<img src='../../../public/add_question.svg' className={styles.questionButtonImg} alt='add question' />
<span className={styles.textButton}>Добавить вопрос</span>
</button>
);
};

View file

@ -7,17 +7,25 @@
}
.textAnswer{
border: none;
background-color: #ffffff;
font-size: 18px;
font-weight: 500;
align-items: center;
}
.answerIcon{
vertical-align: middle;
width: 24px;
}
.answerInput{
vertical-align: middle;
font-size: 18px;
font-weight: 500;
outline: none;
border: none;
resize: none;
display: flex;
align-items: center;
box-sizing: border-box;
}

View file

@ -1,16 +1,21 @@
import React, {useState, useRef, useEffect} from "react";
import styles from'./AnswerOption.module.css';
const single_selected_response = '../../../public/radio_button_checked.svg';
const multiple_selected_response = '../../../public/check_box.svg';
// const single_response =
// const multiple_response =
interface AnswerOptionProps{
src: string;
index: number;
value: string;
onChange: (value: string) => void;
selectedType: 'single' | 'multiply';
}
const AnswerOption: React.FC<AnswerOptionProps> = ({src, index, value, onChange}) => {
const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, selectedType}) => {
const [currentValue, setCurrentValue] = useState(value);
const [isEditing, setIsEditing] = useState(false); //редактируется ли сейчас
const [isEditing, setIsEditing] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
@ -48,22 +53,30 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({src, index, value, onChange}
}
}, [isEditing]);
const getImage = (typeValue: string): string => {
if (typeValue === 'multiply') {
return multiple_selected_response;
} else {
return single_selected_response;
}
};
return (
<div className={styles.answer}>
<img className={styles.answerIcon} src={src} alt="" />
<img className={styles.answerIcon} src={getImage(selectedType)} alt="" />
{isEditing ? (
<textarea className={styles.answerInput}
ref={textAreaRef}
value={currentValue}
onChange={handleTextareaChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
placeholder={`Ответ ${index}`}
ref={textAreaRef}
value={currentValue}
onChange={handleTextareaChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
placeholder={`Ответ ${index}`}
/>
) : (
<span className={styles.textAnswer} onClick={handleSpanClick}>
{currentValue || `Ответ ${index}`}
</span>
<button className={styles.textAnswer} onClick={handleSpanClick}>
{currentValue || `Ответ ${index}`}
</button>
)}
</div>
);

View file

@ -5,5 +5,4 @@
padding: 0;
width: 100%;
display: flex;
/*justify-content: space-between;*/
}

View file

@ -4,9 +4,7 @@ import Account from "../Account/Account.tsx";
import styles from './Header.module.css'
import SurveyPagesList from "../SurveyPagesList/SurveyPagesList.tsx";
interface HeaderProps {}
const Header: React.FC<HeaderProps> = () => {
const Header: React.FC = () => {
const [activePage, setActivePage] = useState('Создать опрос');
const handlePageClick = (name: string)=> {
@ -20,7 +18,10 @@ const Header: React.FC<HeaderProps> = () => {
activePage={activePage}
onPageClick = {handlePageClick}
/>
<Account href='' />
<Account
href=''
user='Иванов Иван'
/>
</div>
);
};

View file

@ -1,9 +1,8 @@
/*Logo.module.css*/
.logo {
margin: 0;
padding: 0;
height: 52px;
width: 52px;
margin: 31px auto 25px 40px;
margin: 31px 77px 25px 40px;
}

View file

@ -8,7 +8,7 @@ interface LogoProps {
const Logo: React.FC<LogoProps> = ({href}) => {
return (
<a className={styles.logo} href={href}>
<img src='../../../public/logo.svg' alt='Логотип'/>
<img src='../../../public/logo.svg' alt="" />
</a>
);
};

View file

@ -1,3 +1,5 @@
/*MainComponent.module.css*/
.mainPage{
width: 100%;
display: flex;

View file

@ -1,5 +1,10 @@
/*Navigation.module.css*/
.navContainer{
display: flex;
flex-direction: column;
}
.nav{
margin: 34px 0 48px 40px;
background-color: white;

View file

@ -12,7 +12,7 @@ const Navigation: React.FC<NavigationProps> = ({onNavigationClick, activePage})
const items: string[] = ['Вопросы', 'Настройки', 'Результаты']
return (
<div>
<div className={styles.navContainer}>
<nav className={styles.nav}>
<ul className={styles.navList}>
{items.map(item => (

View file

@ -1,15 +1,21 @@
/*NavigationItem.module.css*/
.navItem{
font-weight: 600;
font-size: 24px;
color: #AFAFAF;
padding: 0;
margin-bottom: 42px;
}
.page{
background-color: white;
border: none;
font-size: 24px;
font-weight: 600;
color: #AFAFAF;
}
.active{
text-decoration: underline 2px #556FB7;
color: #000000;;
color: #000000;
}
.navItem:last-child{

View file

@ -10,9 +10,9 @@ interface NavigationItemProps{
const NavigationItem: React.FC<NavigationItemProps> = ({title, onClick, isActive}) => {
return (
<li className={styles.navItem}>
<a className={`${styles.page} ${isActive ? styles.active : ''}`} onClick={onClick}>
<button className={`${styles.page} ${isActive ? styles.active : ''}`} onClick={onClick}>
{title}
</a>
</button>
</li>
);
};

View file

@ -1,36 +1,22 @@
/*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;
}
/*.pagesSurveyItem {*/
/* font-size: 24px;*/
/* font-weight: 600;*/
/* color: #2A6DAE;*/
/* position: relative; !* Necessary for positioning the underline *!*/
/* display: inline-block; !* Makes the width only as wide as the content *!*/
/*}*/
/*.active {*/
/* color: #000000;*/
/*}*/
/*.active::after {*/
/* content: "";*/
/* display: block; !* Makes it a block element for width/height control *!*/
/* width: 96px;*/
/* height: 2px; !* Adjust as needed for the thickness of the underline *!*/
/* background-color: #3881C8;*/
/* position: absolute;*/
/* left: 50%; !* Center horizontally *!*/
/* transform: translateX(-50%); !* Corrects the centering *!*/
/* bottom: -5px; !* Position 5px below the text *!*/
/*}*/

View file

@ -10,9 +10,9 @@ interface PageSurveyProps{
const PageSurvey: React.FC<PageSurveyProps> = ({name, isActive, onClick}) => {
return (
<li className={styles.pagesSurveyItem}>
<a className={`${styles.pageSurvey} ${isActive ? styles.active : ''}`} onClick={onClick}>
<button className={`${styles.pageSurvey} ${isActive ? styles.active : ''}`} onClick={onClick}>
{name}
</a>
</button>
</li>
);
};

View file

@ -1,18 +1,40 @@
/*QuestionItem.module.css*/
.questionCard{
width: 100%;
background-color: white;
display: flex;
justify-content: space-between;
margin-bottom: 34px;
padding: 27px 29px 26px 36px;
border-radius: 14px;
}
.questionCard:last-child{
margin-bottom: 0;
}
.questionContainer{
display: flex;
flex-direction: column;
}
.questionTextarea{
border: none;
outline: none;
resize: none;
margin-bottom: 5px;
font-size: 24px;
font-weight: 600;
}
.buttonQuestion{
border: none;
outline: none;
background-color: #ffffff;
padding: 0;
}
.textQuestion{
margin-top: 0;
width: 100%;
font-size: 24px;
font-weight: 600;
margin-bottom: 35px;
text-align: start;
}

View file

@ -1,29 +1,63 @@
import React, {useState} from "react";
import React, {useState, useRef, useEffect} from "react";
import AnswerOption from '../AnswerOption/AnswerOption';
import AddAnswerButton from "../AddAnswerButton/AddAnswerButton.tsx";
import TypeDropdown from "../TypeDropdown/TypeDropdown.tsx";
import styles from './QuestionItem.module.css'
import singleChoiceIcon from '../../../public/radio_button_checked.svg'
import multiplyChoiceIcon from '../../../public/check_box.svg'
interface QuestionItemProps {
indexQuestion: number;
initialTextQuestion?: string;
valueQuestion: string;
onChangeQuestion: (valueQuestion: string) => void;
}
const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQuestion = `Вопрос ${indexQuestion}`}) => {
const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQuestion = `Вопрос ${indexQuestion}`,
valueQuestion, onChangeQuestion}) => {
const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single');
const [answerOption, setAnswerOption] = useState(['']);
const [questionType] = useState('single');
const [textQuestion, setTextQuestion] = useState(initialTextQuestion);
const [isEditingQuestion, setIsEditingQuestion] = useState(false);
const textareaQuestionRef = useRef<HTMLTextAreaElement>(null);
const handleTypeChange = (type: 'single' | 'multiply') => {
setSelectedType(type);
}
const handleAddAnswer = () => {
setAnswerOption([...answerOption, '']);
};
// const handleTypeChange = (type: string) => {
// setQuestionType(type);
// }
const handleQuestionClick = () => {
setIsEditingQuestion(true);
}
const handleTextareaQuestionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setTextQuestion(event.target.value);
}
const handleSaveQuestion = () => {
setIsEditingQuestion(false);
onChangeQuestion(textQuestion);
}
const handleQuestionKeyDown = (keyDownEvent: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (keyDownEvent.key === 'Enter') {
keyDownEvent.preventDefault();
handleSaveQuestion()
}
}
const handleQuestionBlur = () => {
handleSaveQuestion()
}
useEffect(() => {
if (isEditingQuestion && textareaQuestionRef.current) {
textareaQuestionRef.current.focus();
}
}, [isEditingQuestion]);
const handleAnswerChange = (index: number, value: string) => {
const newAnswerOption = [...answerOption];
@ -31,25 +65,34 @@ const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQu
setAnswerOption(newAnswerOption);
}
const handleQuestionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setTextQuestion(event.target.value);
};
useEffect(() => {
setTextQuestion(valueQuestion);
}, [valueQuestion]);
return (
<div className={styles.questionCard}>
<div className={styles.questionContainer}>
<h2>
<textarea
value={textQuestion}
onChange={handleQuestionChange}
/>
</h2>
{isEditingQuestion ? (
<textarea
className={styles.questionTextarea}
ref={textareaQuestionRef}
value={textQuestion === initialTextQuestion ? '' : textQuestion}
onChange={handleTextareaQuestionChange}
onKeyDown={handleQuestionKeyDown}
onBlur={handleQuestionBlur}
placeholder={initialTextQuestion}
/>
) : (
<button className={styles.buttonQuestion} onClick={handleQuestionClick}>
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
</button>
)}
{answerOption.map((answerText, index) => (
<AnswerOption
key={index}
selectedType={selectedType}
index={index + 1} // Индекс ответа
value={answerText}
src={questionType === "single" ? singleChoiceIcon : multiplyChoiceIcon}
onChange={(value) => handleAnswerChange(index, value)}
/>
))}
@ -58,7 +101,7 @@ const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQu
onClick={handleAddAnswer}
/>
</div>
<TypeDropdown/>
<TypeDropdown selectedType={selectedType} onTypeChange={handleTypeChange}/>
</div>
);
}

View file

@ -6,20 +6,28 @@ interface QuestionsListProps {}
interface Question {
id: number;
text: string
}
const QuestionsList: React.FC<QuestionsListProps> = () => {
const [questions, setQuestions] = useState<Question[]>([
{id: 1},
{id: 1, text: ''},
]);
const handleAddQuestion = () => {
// Find the highest ID in the current questions list
const maxId = questions.reduce((max, question) => Math.max(max, question.id), 0);
const newQuestion: Question = {
id: maxId + 1, // Increment the ID
id: maxId + 1,
text: ''
};
setQuestions([...questions, newQuestion]); // Add the new question to the state
setQuestions([...questions, newQuestion]);
};
const handleQuestionChange = (index: number, value: string) => {
const newQuestions = [...questions];
newQuestions[index] = {...newQuestions[index], text: value};
setQuestions(newQuestions);
};
return (
@ -28,6 +36,8 @@ const QuestionsList: React.FC<QuestionsListProps> = () => {
<QuestionItem
key={question.id}
indexQuestion={index + 1}
valueQuestion={''}
onChangeQuestion={(value) => handleQuestionChange(index, value)}
/>
))}
<AddQuestionButton onClick={handleAddQuestion} />

View file

@ -1,7 +1,6 @@
/*SaveButton.module.css*/
.createSurveyButton {
/*width: 15%;*/
margin-left: 40px;
padding: 25px 50.5px;
border: none;
@ -12,4 +11,5 @@
font-size: 24px;
text-align: center;
box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1);
box-sizing: border-box;
}

View file

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

View file

@ -2,55 +2,71 @@
.blockInfo{
background-color: #ffffff;
/*margin: 0;*/
padding: 0;
width: 100%;
margin-top: 34px;
margin-bottom: 49px;
border-radius: 14px;
height: 191px;
min-height: 191px;
}
.info{
min-width: 373px;
display: block;
padding: 35px 0;
padding: 35px; /*подумать нужно ли справа слева отступы*/
}
.titleSurvey{
resize: none;
display: block;
border: none;
margin: 0 auto;
background-color: white;
text-align: center;
margin: 0;
padding: 0 20px;
font-size: 40px;
font-size: 20px;
font-weight: 600;
line-height: 20px;
margin-bottom: 23px;
word-break: break-word;
padding: 0;
}
.textareaTitle{
margin-top: -17px;
width: 80%;
padding-top: 35px;
resize: none;
text-align: center;
font-size: 40px;
font-weight: 600;
margin: 0;
padding: 0;
border: none;
margin-bottom: 23px;
outline: none;
line-height: 30px;
word-break: break-word;
}
.description{
resize: none;
text-align: center;
margin: 0;
padding: 0 20px;
border: none;
font-size: 24px;
font-weight: 500;
text-align: center;
background-color: white;
display: block;
margin: 0 auto;
word-break: break-word;
padding: 0;
}
.textareaDescrip{
width: 80%;
outline: none;
border: none;
resize: none;
text-align: center;
margin: 0;
margin: 0 auto;
font-size: 22px;
font-weight: 500;
word-break: break-word;
padding: 0;
}
.descripButton{
@ -61,11 +77,12 @@
}
.descButtonImg{
vertical-align: middle;
width: 28px;
}
.textButton{
vertical-align: center;
vertical-align: middle;
font-size: 24px;
font-weight: 500;
color: #7D7983;

View file

@ -1,14 +1,13 @@
import React, {useState} from "react";
import React, {useState, useRef, useEffect} from "react";
import styles from './SurveyInfo.module.css'
interface SurveyInfoProps {}
const SurveyInfo: React.FC<SurveyInfoProps> = () => {
const SurveyInfo: React.FC = () => {
const [descriptionSurvey, setDescriptionSurvey] = useState('');
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
const [showDescriptionField, setShowDescriptionField] = useState(false);
const [showNewTitleField, setShowNewTitleField] = useState(false);
const titleTextareaRef = useRef<HTMLTextAreaElement>(null);
const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescriptionSurvey(descripEvent.target.value);
@ -22,10 +21,22 @@ const SurveyInfo: React.FC<SurveyInfoProps> = () => {
setShowNewTitleField(true);
}
useEffect(() => {
if (showNewTitleField && titleTextareaRef.current) {
titleTextareaRef.current.focus();
}
}, [showNewTitleField]);
const handleAddDescriptionClick = () => {
setShowDescriptionField(true);
}
useEffect(() => {
if (showDescriptionField && descriptionTextareaRef.current){
descriptionTextareaRef.current.focus();
}
}, [showDescriptionField]);
const handleTitleKeyDown = (titleClickEnter: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (titleClickEnter.key === 'Enter'){
titleClickEnter.preventDefault();
@ -44,6 +55,49 @@ const SurveyInfo: React.FC<SurveyInfoProps> = () => {
setShowDescriptionField(true);
}
const handleTitleBlur = () => {
setShowNewTitleField(false);
};
const handleDescriptionBlur = () => {
setShowDescriptionField(false);
};
const renderDescription = ()=> {
if (descriptionSurvey && !showDescriptionField) {
return (
<button className={styles.description} onClick={handleParagraphClick}>
{descriptionSurvey}
</button>
);
} else if (showDescriptionField) {
return (
<p className={styles.description}>
<textarea
ref={descriptionTextareaRef}
className={styles.textareaDescrip}
value={descriptionSurvey}
placeholder={'Добавить описание'}
onChange={handleDescriptionChange}
onKeyDown={handleDescriptionKeyDown}
onBlur={handleDescriptionBlur}
/>
</p>
);
} else {
return (
<button
className={styles.descripButton}
onClick={handleAddDescriptionClick}
>
<span className={styles.textButton}>Добавить описание</span>
<img src='../../../public/add_circle.svg' className={styles.descButtonImg} alt='add circle'/>
</button>
);
}
}
return (
<div className={styles.blockInfo}>
<div className={styles.info}>
@ -51,34 +105,23 @@ const SurveyInfo: React.FC<SurveyInfoProps> = () => {
showNewTitleField ? (
<h1 className={styles.titleSurvey}>
<textarea className={styles.textareaTitle}
value={titleSurvey}
onChange={handleNewTitleChange}
onKeyDown={handleTitleKeyDown}
ref={titleTextareaRef}
value={titleSurvey === 'Название опроса' ? '' : titleSurvey}
placeholder={'Название опроса'}
onChange={handleNewTitleChange}
onKeyDown={handleTitleKeyDown}
onBlur={handleTitleBlur}
/>
</h1>
) : (
<h1 className={styles.titleSurvey} onClick={handleAddNewTitleClick}>{titleSurvey}</h1>
<button className={styles.titleSurvey} onClick={handleAddNewTitleClick}>
<h1>{titleSurvey || 'Название опроса'}</h1>
</button>
)
}
{descriptionSurvey && !showDescriptionField ? (
<p className={styles.description} onClick={handleParagraphClick}>{descriptionSurvey}</p>
) : showDescriptionField ? (
<p className={styles.description}>
<textarea className={styles.textareaDescrip}
value={descriptionSurvey}
onChange={handleDescriptionChange}
onKeyDown={handleDescriptionKeyDown}
/>
</p>
) : (
<button
className={styles.descripButton}
onClick={handleAddDescriptionClick}>
<span className={styles.textButton}>Добавить описание</span>
<img className={styles.descButtonImg} src='../../../public/add_circle.svg'/>
</button>
)}
{renderDescription()}
</div>
</div>
);

View file

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

View file

@ -1,9 +1,10 @@
/*TypeDropdown.module.css*/
.dropdownContainer {
width: 22%;
width: 23%;
position: relative;
display: inline-block;
margin-right: 29px;
}
.dropdownButton {
@ -16,54 +17,52 @@
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
width: 118%;
text-align: left;
white-space: nowrap;
}
.selectedTypeIcon {
margin-right: 5px;
margin-right: 4px;
}
.dropdownArrow {
margin-left: 5px;
margin-left: auto;
}
.dropdownList {
margin-top: 11px;
position: absolute;
top: 100%;
left: 0;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px 0;
padding: 12px;
list-style: none;
/*margin: 0;*/
z-index: 1; /* Убедитесь, что список отображается поверх других элементов */
width: 100%; /* Занимает всю ширину контейнера */
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
z-index: 1;
width: 100%;
box-shadow: 0 0 4.7px 0 #00000040;
}
.dropdownItem {
padding: 10px 15px;
cursor: pointer;
display: flex;
align-items: center;
}
margin-bottom: 14px;
padding: 0;
border: none;
background-color: #ffffff;
.dropdownItem:hover {
background-color: #f0f0f0;
/*cursor: pointer;*/
}
.dropdownItemIcon {
margin-right: 5px;
width: 24px;
}
.selectedTypeIcon,
.dropdownItemIcon {
width: 20px; /* Задайте нужную ширину и высоту */
width: 20px;
height: 20px;
margin-right: 5px;
vertical-align: middle; /* Выровняйте значок по вертикали */
vertical-align: middle;
}

View file

@ -5,18 +5,24 @@ import styles from './TypeDropdown.module.css'
const single_selected = '../../../public/radio_button_checked.svg';
const multiple_selected = '../../../public/check_box.svg';
const arrow_drop_down = '../../../public/arrow_drop_down.svg';
const arrow_drop_up = '../../../public/arrow_drop_up.svg';
const TypeDropdown: React.FC = () => {
interface TypeDropdownProps {
selectedType: 'single' | 'multiply';
onTypeChange: (type: 'single' | 'multiply') => void;
}
const TypeDropdown: React.FC<TypeDropdownProps> = ({selectedType, onTypeChange}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedType, setSelectedType] = useState('single');
const dropdownRef = useRef<HTMLDivElement>(null);
const handleToggle = () =>{
setIsOpen(!isOpen);
}
const handleSelect = (value: string) => {
setSelectedType(value);
const handleSelect = (value: 'single' | 'multiply') => {
onTypeChange(value);
setIsOpen(false);
}
@ -33,7 +39,7 @@ const TypeDropdown: React.FC = () => {
};
}, [dropdownRef]);
const getImage = (typeValue: string, isSelected: boolean): string => {
const getImage = (typeValue: string): string => {
if (typeValue === 'multiply') {
return multiple_selected;
} else {
@ -45,37 +51,40 @@ const TypeDropdown: React.FC = () => {
<div className={styles.dropdownContainer} ref={dropdownRef}>
<button className={styles.dropdownButton} onClick={handleToggle}>
<img
src={getImage(selectedType, true)}
src={getImage(selectedType)}
alt={selectedType === "single" ? "Одиночный выбор" : "Множественный выбор"}
className={styles.selectedTypeIcon}
/>
{selectedType === "single" ? "Одиночный выбор" : "Множественный выбор"}
<span className={styles.dropdownArrow}></span>
<img
src={isOpen ? arrow_drop_up : arrow_drop_down}
alt={isOpen ? "Закрыть" : "Открыть"}
className={styles.dropdownArrow}
/>
</button>
{isOpen && (
<ul className={styles.dropdownList}>
<li
className={styles.dropdownItem}
onClick={() => handleSelect("single")}
>
<img
src={getImage("single", selectedType === "single")}
alt="Одиночный выбор"
className={styles.dropdownItemIcon}
/>
Одиночный выбор
<li>
<button onClick={() => handleSelect("single")}
className={styles.dropdownItem}>
<img
className={styles.dropdownItemIcon}
src={getImage("single")}
alt=""/>{' '}
Одиночный выбор
</button>
</li>
<li
className={styles.dropdownItem}
onClick={() => handleSelect("multiply")}
>
<img
src={getImage("multiply", selectedType === "multiply")}
alt="Множественный выбор"
className={styles.dropdownItemIcon}
/>
Множественный выбор
<li>
<button className={styles.dropdownItem}
onClick={() => handleSelect("multiply")}>
<img
src={getImage("multiply")}
alt="Множественный выбор"
className={styles.dropdownItemIcon}
/>{' '}
Множественный выбор
</button>
</li>
</ul>
)}

View file

@ -1,68 +1,16 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-family: Monserrat, sans-serif;
line-height: 100%;
font-weight: 600;
font-size: 24px;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
color: #000000;
background-color: white;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
margin: 0;
display: flex;
place-items: center;
width: 100%;
}

View file

@ -4,7 +4,7 @@ import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
<StrictMode>
<App />
</StrictMode>,
)

View file

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

5
SurveyFrontend/src/svg.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module "*.svg" {
import React from 'react';
const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
export default ReactComponent;
}