Merge branch 'spliting_components' into 'unstable'

add components frontend

See merge request internship-2025/survey-webapp/survey-webapp!1
This commit is contained in:
Вячеслав 2025-04-04 16:46:42 +00:00
commit a27087d681
40 changed files with 966 additions and 0 deletions

View file

@ -0,0 +1,9 @@
/*Account.module.css*/
.account {
margin: 0;
padding: 0;
width: 52px;
height: 52px;
margin: 31px 39px 25px 0;
}

View file

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

View file

@ -0,0 +1,16 @@
/*AddAnswerButton.module.css*/
.answerButton {
display: flex;
gap: 10px;
align-items: center;
border: none;
background-color: white;
color: #3788D6;
font-size: 18px;
font-weight: 500;
}
.addAnswerImg{
vertical-align: middle;
}

View file

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

View file

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

View file

@ -0,0 +1,17 @@
import React from "react";
import styles from './AddQuestionButton.module.css'
interface AddQuestionButtonProps {
onClick: () => void;
}
const AddQuestionButton: React.FC<AddQuestionButtonProps> = ({onClick}) => {
return (
<button className={styles.questionButton} onClick={onClick}>
<img className={styles.questionButtonImg} src='../../../public/add_question.svg' alt=''/>
Добавить вопрос
</button>
);
};
export default AddQuestionButton;

View file

@ -0,0 +1,23 @@
/*AnswerOption.module.css*/
.answer{
display: flex;
gap: 10px;
margin-bottom: 17px;
}
.textAnswer{
font-size: 18px;
font-weight: 500;
align-items: center;
}
.answerIcon{
vertical-align: middle;
}
.answerInput{
outline: none;
border: none;
resize: none;
}

View file

@ -0,0 +1,72 @@
import React, {useState, useRef, useEffect} from "react";
import styles from'./AnswerOption.module.css';
interface AnswerOptionProps{
src: string;
index: number;
value: string;
onChange: (value: string) => void;
}
const AnswerOption: React.FC<AnswerOptionProps> = ({src, index, value, onChange}) => {
const [currentValue, setCurrentValue] = useState(value);
const [isEditing, setIsEditing] = useState(false); //редактируется ли сейчас
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setCurrentValue(value);
}, [value]);
const handleSpanClick = () => {
setIsEditing(true);
}
const handleTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setCurrentValue(event.target.value);
};
const handleSave = () => {
setIsEditing(false);
onChange(currentValue); // Отправляем измененное значение родителю
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter") {
event.preventDefault(); // Предотвращаем перенос строки в textarea
handleSave();
}
};
const handleBlur = () => {
handleSave();
};
useEffect(() => {
if (isEditing && textAreaRef.current) {
textAreaRef.current.focus();
}
}, [isEditing]);
return (
<div className={styles.answer}>
<img className={styles.answerIcon} src={src} alt="" />
{isEditing ? (
<textarea className={styles.answerInput}
ref={textAreaRef}
value={currentValue}
onChange={handleTextareaChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
placeholder={`Ответ ${index}`}
/>
) : (
<span className={styles.textAnswer} onClick={handleSpanClick}>
{currentValue || `Ответ ${index}`}
</span>
)}
</div>
);
};
export default AnswerOption;

View file

@ -0,0 +1,9 @@
/*Header.module.css*/
.header{
margin: 0;
padding: 0;
width: 100%;
display: flex;
/*justify-content: space-between;*/
}

View file

@ -0,0 +1,28 @@
import React, {useState} 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";
interface HeaderProps {}
const Header: React.FC<HeaderProps> = () => {
const [activePage, setActivePage] = useState('Создать опрос');
const handlePageClick = (name: string)=> {
setActivePage(name);
}
return (
<div className={styles.header}>
<Logo href='' />
<SurveyPagesList
activePage={activePage}
onPageClick = {handlePageClick}
/>
<Account href='' />
</div>
);
};
export default Header;

View file

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

View file

@ -0,0 +1,16 @@
import React from "react";
import styles from './Logo.module.css'
interface LogoProps {
href: string;
}
const Logo: React.FC<LogoProps> = ({href}) => {
return (
<a className={styles.logo} href={href}>
<img src='../../../public/logo.svg' alt='Логотип'/>
</a>
);
};
export default Logo;

View file

@ -0,0 +1,5 @@
.mainPage{
width: 100%;
display: flex;
background-color: #F6F6F6;
}

View file

@ -0,0 +1,24 @@
import Navigation from "../Navigation/Navigation.tsx";
import React, {useState} from "react";
import styles from './MainComponent.module.css'
import Survey from "../Survey/Survey.tsx";
const MainComponent: React.FC = () => {
const [activePage, setActivePage] = useState('Вопросы');
const handleNavigationClick = (title: string) => {
setActivePage(title);
}
return (
<main className={styles.mainPage}>
<Navigation
activePage={activePage}
onNavigationClick={handleNavigationClick}
/>
<Survey />
</main>
)
}
export default MainComponent;

View file

@ -0,0 +1,12 @@
/*Navigation.module.css*/
.nav{
margin: 34px 0 48px 40px;
background-color: white;
border-radius: 20px;
}
.navList{
list-style: none;
padding: 52px 57px 70px 36px;
}

View file

@ -0,0 +1,34 @@
import React from 'react'
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<NavigationProps> = ({onNavigationClick, activePage}) => {
const items: string[] = ['Вопросы', 'Настройки', 'Результаты']
return (
<div>
<nav className={styles.nav}>
<ul className={styles.navList}>
{items.map(item => (
<NavigationItem
key={item}
title={item}
isActive={activePage === item}
onClick={() => onNavigationClick(item)}
/>
))}
</ul>
</nav>
<SaveButton />
</div>
);
};
export default Navigation;

View file

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

View file

@ -0,0 +1,20 @@
import React from 'react'
import styles from './NavigationItem.module.css'
interface NavigationItemProps{
title: string;
onClick(): void;
isActive: boolean; //Дописать для активной ссылки, для класса
}
const NavigationItem: React.FC<NavigationItemProps> = ({title, onClick, isActive}) => {
return (
<li className={styles.navItem}>
<a className={`${styles.page} ${isActive ? styles.active : ''}`} onClick={onClick}>
{title}
</a>
</li>
);
};
export default NavigationItem;

View file

@ -0,0 +1,36 @@
.pagesSurveyItem{
font-size: 24px;
font-weight: 600;
color: #2A6DAE;
}
.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

@ -0,0 +1,20 @@
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}>
<a className={`${styles.pageSurvey} ${isActive ? styles.active : ''}`} onClick={onClick}>
{name}
</a>
</li>
);
};
export default PageSurvey;

View file

@ -0,0 +1,18 @@
/*QuestionItem.module.css*/
.questionCard{
width: 100%;
background-color: white;
display: flex;
margin-bottom: 34px;
padding: 27px 29px 26px 36px;
border-radius: 14px;
}
.questionCard:last-child{
margin-bottom: 0;
}
.questionContainer{
}

View file

@ -0,0 +1,66 @@
import React, {useState} 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;
}
const QuestionItem: React.FC<QuestionItemProps> = ({indexQuestion, initialTextQuestion = `Вопрос ${indexQuestion}`}) => {
const [answerOption, setAnswerOption] = useState(['']);
const [questionType] = useState('single');
const [textQuestion, setTextQuestion] = useState(initialTextQuestion);
const handleAddAnswer = () => {
setAnswerOption([...answerOption, '']);
};
// const handleTypeChange = (type: string) => {
// setQuestionType(type);
// }
const handleAnswerChange = (index: number, value: string) => {
const newAnswerOption = [...answerOption];
newAnswerOption[index] = value;
setAnswerOption(newAnswerOption);
}
const handleQuestionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setTextQuestion(event.target.value);
};
return (
<div className={styles.questionCard}>
<div className={styles.questionContainer}>
<h2>
<textarea
value={textQuestion}
onChange={handleQuestionChange}
/>
</h2>
{answerOption.map((answerText, index) => (
<AnswerOption
key={index}
index={index + 1} // Индекс ответа
value={answerText}
src={questionType === "single" ? singleChoiceIcon : multiplyChoiceIcon}
onChange={(value) => handleAnswerChange(index, value)}
/>
))}
<AddAnswerButton
onClick={handleAddAnswer}
/>
</div>
<TypeDropdown/>
</div>
);
}
export default QuestionItem;

View file

@ -0,0 +1,2 @@
/*QuestionsList.module.css*/

View file

@ -0,0 +1,38 @@
import React, {useState} from "react";
import QuestionItem from "../QuestionItem/QuestionItem.tsx";
import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx";
interface QuestionsListProps {}
interface Question {
id: number;
}
const QuestionsList: React.FC<QuestionsListProps> = () => {
const [questions, setQuestions] = useState<Question[]>([
{id: 1},
]);
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
};
setQuestions([...questions, newQuestion]); // Add the new question to the state
};
return (
<>
{questions.map((question, index) => (
<QuestionItem
key={question.id}
indexQuestion={index + 1}
/>
))}
<AddQuestionButton onClick={handleAddQuestion} />
</>
);
};
export default QuestionsList;

View file

@ -0,0 +1,15 @@
/*SaveButton.module.css*/
.createSurveyButton {
/*width: 15%;*/
margin-left: 40px;
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);
}

View file

@ -0,0 +1,16 @@
import React from 'react'
import styles from './SaveButton.module.css'
interface CreateSurveyButtonProps {
// onClick(): void;
}
const SaveButton: React.FC<CreateSurveyButtonProps> = ({}) => {
return (
<button className={styles.createSurveyButton}>
Сохранить
</button>
);
}
export default SaveButton;

View file

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

View file

@ -0,0 +1,15 @@
import React from "react";
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
import QuestionsList from "../QuestionsList/QuestionsList.tsx";
import styles from './Survey.module.css'
const Survey: React.FC = () => {
return (
<div className={styles.survey}>
<SurveyInfo />
<QuestionsList />
</div>
);
}
export default Survey;

View file

@ -0,0 +1,73 @@
/*SurveyInfo.module.css*/
.blockInfo{
background-color: #ffffff;
/*margin: 0;*/
padding: 0;
width: 100%;
margin-top: 34px;
margin-bottom: 49px;
border-radius: 14px;
height: 191px;
}
.info{
display: block;
padding: 35px 0;
}
.titleSurvey{
resize: none;
text-align: center;
margin: 0;
padding: 0 20px;
font-size: 40px;
font-weight: 600;
margin-bottom: 23px;
}
.textareaTitle{
resize: none;
text-align: center;
font-size: 40px;
font-weight: 600;
margin: 0;
padding: 0;
border: none;
margin-bottom: 23px;
}
.description{
resize: none;
text-align: center;
margin: 0;
padding: 0 20px;
font-size: 24px;
font-weight: 500;
}
.textareaDescrip{
resize: none;
text-align: center;
margin: 0;
font-size: 22px;
}
.descripButton{
border: none;
background-color: #ffffff;
display: block;
margin: 0 auto;
}
.descButtonImg{
width: 28px;
}
.textButton{
vertical-align: center;
font-size: 24px;
font-weight: 500;
color: #7D7983;
padding: 10px;
}

View file

@ -0,0 +1,87 @@
import React, {useState} from "react";
import styles from './SurveyInfo.module.css'
interface SurveyInfoProps {}
const SurveyInfo: React.FC<SurveyInfoProps> = () => {
const [descriptionSurvey, setDescriptionSurvey] = useState('');
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
const [showDescriptionField, setShowDescriptionField] = useState(false);
const [showNewTitleField, setShowNewTitleField] = useState(false);
const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescriptionSurvey(descripEvent.target.value);
}
const handleNewTitleChange = (titleEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitleSurvey(titleEvent.target.value);
}
const handleAddNewTitleClick = () => {
setShowNewTitleField(true);
}
const handleAddDescriptionClick = () => {
setShowDescriptionField(true);
}
const handleTitleKeyDown = (titleClickEnter: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (titleClickEnter.key === 'Enter'){
titleClickEnter.preventDefault();
setShowNewTitleField(false);
}
}
const handleDescriptionKeyDown = (descripClickEnter: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (descripClickEnter.key === 'Enter'){
descripClickEnter.preventDefault();
setShowDescriptionField(false);
}
}
const handleParagraphClick = () => {
setShowDescriptionField(true);
}
return (
<div className={styles.blockInfo}>
<div className={styles.info}>
{
showNewTitleField ? (
<h1 className={styles.titleSurvey}>
<textarea className={styles.textareaTitle}
value={titleSurvey}
onChange={handleNewTitleChange}
onKeyDown={handleTitleKeyDown}
/>
</h1>
) : (
<h1 className={styles.titleSurvey} onClick={handleAddNewTitleClick}>{titleSurvey}</h1>
)
}
{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>
)}
</div>
</div>
);
};
export default SurveyInfo;

View file

@ -0,0 +1,7 @@
.listSurveyPages{
display: flex;
gap: 61px;
list-style: none;
align-items: center;
margin-right: 900px;
}

View file

@ -0,0 +1,27 @@
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

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

View file

@ -0,0 +1,87 @@
import React, { useState, useRef, useEffect } from "react";
import styles from './TypeDropdown.module.css'
const single_selected = '../../../public/radio_button_checked.svg';
const multiple_selected = '../../../public/check_box.svg';
const TypeDropdown: React.FC = () => {
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);
setIsOpen(false);
}
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [dropdownRef]);
const getImage = (typeValue: string, isSelected: boolean): string => {
if (typeValue === 'multiply') {
return multiple_selected;
} else {
return single_selected;
}
};
return (
<div className={styles.dropdownContainer} ref={dropdownRef}>
<button className={styles.dropdownButton} onClick={handleToggle}>
<img
src={getImage(selectedType, true)}
alt={selectedType === "single" ? "Одиночный выбор" : "Множественный выбор"}
className={styles.selectedTypeIcon}
/>
{selectedType === "single" ? "Одиночный выбор" : "Множественный выбор"}
<span className={styles.dropdownArrow}></span>
</button>
{isOpen && (
<ul className={styles.dropdownList}>
<li
className={styles.dropdownItem}
onClick={() => handleSelect("single")}
>
<img
src={getImage("single", selectedType === "single")}
alt="Одиночный выбор"
className={styles.dropdownItemIcon}
/>
Одиночный выбор
</li>
<li
className={styles.dropdownItem}
onClick={() => handleSelect("multiply")}
>
<img
src={getImage("multiply", selectedType === "multiply")}
alt="Множественный выбор"
className={styles.dropdownItemIcon}
/>
Множественный выбор
</li>
</ul>
)}
</div>
);
};
export default TypeDropdown;

View file

@ -0,0 +1,13 @@
import Header from '../components/Header/Header.tsx'
import MainComponents from '../components/MainComponent/MainComponent.tsx'
const Questions = () => {
return (
<>
<Header />
<MainComponents />
</>
);
};
export default Questions;

View file

View file