7.5. Survey - Компоненты UI
Обзор компонентов UI
Приложение Survey содержит ряд UI-компонентов, которые обеспечивают пользовательский интерфейс приложения. Компоненты разработаны с учетом принципов доступности, адаптивности и переиспользуемости.
Основные UI-компоненты
Header - Заголовок
Компонент Header отображает заголовок опроса и прогресс прохождения.
Файл: src/components/Header.tsx
Пример кода Header
import React from 'react';
import Logo from './Logo';
import './Header.css';
interface HeaderProps {
title: string;
currentQuestion: number;
totalQuestions: number;
companyName?: string;
}
const Header: React.FC<HeaderProps> = ({
title,
currentQuestion,
totalQuestions,
companyName
}) => {
// Расчет процента прогресса
const progressPercentage = (currentQuestion / totalQuestions) * 100;
return (
<header className="survey-header">
<div className="header-top">
<div className="header-logo">
<Logo />
</div>
{companyName && (
<div className="company-name">
{companyName}
</div>
)}
</div>
<div className="header-title">
{title}
</div>
<div className="progress-container">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progressPercentage}%` }}
></div>
</div>
<div className="progress-text">
Вопрос {currentQuestion} из {totalQuestions}
</div>
</div>
</header>
);
};
export default Header;
Logo - Логотип
Компонент Logo отображает логотип приложения.
Файл: src/components/Logo.tsx
Пример кода Logo
import React from 'react';
import './Logo.css';
const Logo: React.FC = () => {
return (
<div className="logo">
{/* SVG логотип */}
<svg width="100" height="32" viewBox="0 0 100 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 6L23 12V24L12.5 30L2 24V12L12.5 6Z" fill="#7D4CF1"/>
<path d="M12.5 16L18 19V25L12.5 28L7 25V19L12.5 16Z" fill="white"/>
<text x="30" y="22" fill="#333333" fontFamily="Arial, sans-serif" fontSize="16" fontWeight="bold">Survey</text>
</svg>
</div>
);
};
export default Logo;
Footer - Подвал
Компонент Footer отображает информацию в нижней части страницы.
Файл: src/components/Footer.tsx
Пример кода Footer
import React from 'react';
import './Footer.css';
interface FooterProps {
companyName?: string;
}
const Footer: React.FC<FooterProps> = ({ companyName }) => {
const year = new Date().getFullYear();
return (
<footer className="survey-footer">
<div className="footer-content">
<p>
{companyName
? `© ${year} ${companyName}. Все права защищены.`
: `© ${year} Survey. Все права защищены.`}
</p>
<p className="privacy-note">
Ваши ответы анонимны и используются только для улучшения рабочей среды.
</p>
</div>
</footer>
);
};
export default Footer;
Интерфейсы ответов
Для различных типов вопросов в опросе реализованы различные интерфейсы ответов.
Radio5pt - Радиокнопки (5 баллов)
Компонент Radio5pt представляет собой 5-балльную шкалу с радиокнопками.
Файл: src/components/answer-interfaces/radio-5pt/Radio5pt.tsx
Пример кода Radio5pt
import React from 'react';
import './Radio5pt.css';
interface Radio5ptProps {
options: string[];
value: number | null;
onChange: (value: number) => void;
}
const Radio5pt: React.FC<Radio5ptProps> = ({ options, value, onChange }) => {
return (
<div className="radio-5pt">
<div className="options-container">
{options.map((option, index) => (
<div
key={index}
className={`option ${value === index + 1 ? 'selected' : ''}`}
onClick={() => onChange(index + 1)}
>
<div className="radio-button">
<div className="radio-inner"></div>
</div>
<div className="option-label">{option}</div>
</div>
))}
</div>
</div>
);
};
export default Radio5pt;
Stars5pt - Звезды (5 баллов)
Компонент Stars5pt представляет собой 5-балльную шкалу в виде звезд.
Файл: src/components/answer-interfaces/stars-5pt/Stars5pt.tsx
Пример кода Stars5pt
import React from 'react';
import './Stars5pt.css';
interface Stars5ptProps {
value: number | null;
onChange: (value: number) => void;
}
const Stars5pt: React.FC<Stars5ptProps> = ({ value, onChange }) => {
const handleClick = (rating: number) => {
onChange(rating);
};
const renderStarLabel = (rating: number) => {
switch (rating) {
case 1:
return 'Очень плохо';
case 2:
return 'Плохо';
case 3:
return 'Нормально';
case 4:
return 'Хорошо';
case 5:
return 'Отлично';
default:
return '';
}
};
return (
<div className="stars-5pt">
<div className="stars-container">
{[1, 2, 3, 4, 5].map((rating) => (
<div
key={rating}
className={`star ${value && rating <= value ? 'active' : ''}`}
onClick={() => handleClick(rating)}
>
★
</div>
))}
</div>
{value && (
<div className="star-label">
{renderStarLabel(value)}
</div>
)}
</div>
);
};
export default Stars5pt;
Scale11pt - Шкала (11 баллов)
Компонент Scale11pt представляет собой 11-балльную шкалу (от 0 до 10).
Файл: src/components/answer-interfaces/scale-11pt/Scale11pt.tsx
Пример кода Scale11pt
import React from 'react';
import './Scale11pt.css';
interface Scale11ptProps {
value: number | null;
onChange: (value: number) => void;
}
const Scale11pt: React.FC<Scale11ptProps> = ({ value, onChange }) => {
return (
<div className="scale-11pt">
<div className="scale-container">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((rating) => (
<div
key={rating}
className={`scale-item ${value === rating ? 'selected' : ''}`}
onClick={() => onChange(rating)}
>
{rating}
</div>
))}
</div>
<div className="scale-labels">
<div>Совсем не вероятно</div>
<div>Очень вероятно</div>
</div>
</div>
);
};
export default Scale11pt;
Slider11pt - Ползунок (11 баллов)
Компонент Slider11pt представляет собой 11-балльную шкалу (от 0 до 10) с ползунком.
Файл: src/components/answer-interfaces/slider-11pt/Slider11pt.tsx
Пример кода Slider11pt
import React, { useState, useEffect } from 'react';
import './Slider11pt.css';
interface Slider11ptProps {
value: number | null;
onChange: (value: number) => void;
}
const Slider11pt: React.FC<Slider11ptProps> = ({ value, onChange }) => {
const [sliderValue, setSliderValue] = useState(value || 5);
useEffect(() => {
if (value !== null) {
setSliderValue(value);
}
}, [value]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseInt(e.target.value, 10);
setSliderValue(newValue);
};
const handleChangeEnd = () => {
onChange(sliderValue);
};
return (
<div className="slider-11pt">
<div className="slider-container">
<input
type="range"
min="0"
max="10"
value={sliderValue}
onChange={handleChange}
onMouseUp={handleChangeEnd}
onTouchEnd={handleChangeEnd}
className="slider"
/>
<div className="slider-markers">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((mark) => (
<div
key={mark}
className="slider-marker"
onClick={() => {
setSliderValue(mark);
onChange(mark);
}}
>
<div className="marker-tick"></div>
<div className="marker-value">{mark}</div>
</div>
))}
</div>
<div className="slider-labels">
<div>Совсем не вероятно</div>
<div>Очень вероятно</div>
</div>
</div>
</div>
);
};
export default Slider11pt;
Text - Текстовое поле
Компонент Text представляет собой текстовое поле для ввода ответа.
Файл: src/components/answer-interfaces/text/Text.tsx
Пример кода Text
import React, { useState, useEffect } from 'react';
import './Text.css';
interface TextProps {
value: string | null;
onChange: (value: string) => void;
}
const Text: React.FC<TextProps> = ({ value, onChange }) => {
const [text, setText] = useState(value || '');
useEffect(() => {
if (value !== null) {
setText(value);
}
}, [value]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setText(newValue);
};
const handleBlur = () => {
onChange(text);
};
return (
<div className="text-input">
<textarea
value={text}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Введите ваш ответ здесь..."
rows={4}
/>
<div className="text-counter">
{text.length} / 500 символов
</div>
</div>
);
};
export default Text;
Pictures4pt - Выбор картинки (4 варианта)
Компонент Pictures4pt представляет собой выбор одного изображения из четырех вариантов.
Файл: src/components/answer-interfaces/pictures-4pt/Pictures4pt.tsx
Пример кода Pictures4pt
import React from 'react';
import './Pictures4pt.css';
interface Pictures4ptProps {
options: string[];
value: number | null;
onChange: (value: number) => void;
}
const Pictures4pt: React.FC<Pictures4ptProps> = ({ options, value, onChange }) => {
// Иконки для опций (можно заменить на реальные изображения)
const icons = [
'😀', // Счастливый
'😐', // Нейтральный
'😔', // Грустный
'😡', // Злой
];
return (
<div className="pictures-4pt">
<div className="pictures-container">
{options.map((option, index) => (
<div
key={index}
className={`picture-option ${value === index + 1 ? 'selected' : ''}`}
onClick={() => onChange(index + 1)}
>
<div className="picture-icon">{icons[index]}</div>
<div className="picture-label">{option}</div>
</div>
))}
</div>
</div>
);
};
export default Pictures4pt;
Стилизация UI-компонентов
Стилизация UI-компонентов реализована с использованием CSS-файлов для каждого компонента и библиотеки TailwindCSS.
Пример CSS для компонента Stars5pt
Пример CSS для Stars5pt
/* src/components/answer-interfaces/stars-5pt/Stars5pt.css */
.stars-5pt {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
}
.stars-container {
display: flex;
justify-content: center;
gap: 16px;
margin-bottom: 16px;
}
.star {
font-size: 42px;
cursor: pointer;
color: #ddd;
transition: color 0.3s, transform 0.2s;
}
.star:hover {
transform: scale(1.1);
}
.star.active {
color: #FFD700;
}
.star-label {
font-size: 18px;
font-weight: 500;
color: #333;
}
Основные UI-стили с TailwindCSS
Основные стили и утилиты для UI-компонентов определены с помощью TailwindCSS.
Пример использования TailwindCSS
// Пример компонента с использованием TailwindCSS
const CompletionScreen: React.FC<CompletionScreenProps> = ({ companyName, onRestart }) => {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 p-4">
<div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8 text-center">
<div className="text-4xl mb-4 text-green-500">✓</div>
<h2 className="text-2xl font-bold text-gray-800 mb-4">
Опрос успешно завершен
</h2>
<p className="text-gray-600 mb-6">
Благодарим вас за участие! Ваши ответы помогут {companyName || 'нам'} стать лучше.
</p>
<button
onClick={onRestart}
className="px-6 py-2 bg-primary text-white font-medium rounded-md hover:bg-primary-dark transition-colors"
>
Вернуться на главную
</button>
</div>
</div>
);
};
Адаптивный дизайн
Все UI-компоненты в приложении Survey реализованы с учетом адаптивного дизайна, что обеспечивает корректное отображение на устройствах с различными размерами экранов.
Основные принципы адаптивного дизайна в проекте:
- Использование относительных единиц измерения (%, em, rem)
- Применение CSS Flexbox и Grid для адаптивного расположения элементов
- Использование медиа-запросов для настройки стилей на разных размерах экрана
- Оптимизация взаимодействия для сенсорных устройств
Пример адаптивного стиля
/* Адаптивные стили для SurveyCard */
.survey-card {
max-width: 800px;
width: 100%;
margin: 0 auto;
padding: 20px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* Медиа-запросы для адаптации на мобильных устройствах */
@media (max-width: 768px) {
.survey-card {
padding: 16px;
border-radius: 8px;
}
.header-title {
font-size: 18px;
}
.progress-text {
font-size: 14px;
}
}
@media (max-width: 480px) {
.survey-card {
padding: 12px;
}
.header-top {
flex-direction: column;
gap: 8px;
}
.stars-container {
gap: 8px;
}
.star {
font-size: 32px;
}
}
Доступность (Accessibility)
В проекте Survey уделено внимание доступности UI-компонентов для пользователей с различными особенностями:
- Использование семантических HTML-тегов
- Добавление атрибутов ARIA для улучшения доступности
- Обеспечение достаточного контраста цветов
- Поддержка навигации с помощью клавиатуры
Пример реализации доступности
// Пример компонента с улучшенной доступностью
const Scale11pt: React.FC<Scale11ptProps> = ({ value, onChange }) => {
// Расчет процента прогресса
const progressPercentage = (value / 10) * 100;
return (
<div className="scale-11pt" role="group" aria-label="Шкала оценки от 0 до 10">
<div className="scale-container">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((rating) => (
<button
key={rating}
className={`scale-item ${value === rating ? 'selected' : ''}`}
onClick={() => onChange(rating)}
aria-pressed={value === rating}
aria-label={`Оценка ${rating}`}
tabIndex={0}
>
{rating}
</button>
))}
</div>
<div className="scale-labels" aria-hidden="true">
<div>Совсем не вероятно</div>
<div>Очень вероятно</div>
</div>
</div>
);
};
export default Scale11pt;