Темный режим

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;