Skip to content

Abylimpics/5

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

Методичка по выполнению модуля А (Фронтенд)

Часть 5: Страница "Участники" с поиском и фильтрами

Цель: Создать страницу со списком участников, фильтрами по имени, компетенции, категории и региону.

Шаги:

  1. Создание данных об участниках Создайте файл src/data/participants.js:

    export const participants = [
      {
        id: 1,
        fullName: 'Иванов Александр Сергеевич',
        competence: 'Веб-разработка',
        category: 'Студент',
        region: 'Москва',
        university: 'МГТУ им. Баумана',
        photo: 'https://via.placeholder.com/150/3498db/ffffff?text=ИА',
        description: 'Занимается веб-разработкой 3 года, участвует в хакатонах'
      },
      {
        id: 2,
        fullName: 'Петрова Мария Ивановна',
        competence: 'Графический дизайн',
        category: 'Школьник',
        region: 'Санкт-Петербург',
        university: 'Лицей №239',
        photo: 'https://via.placeholder.com/150/2ecc71/ffffff?text=ПМ',
        description: 'Победитель региональных конкурсов дизайна'
      },
      {
        id: 3,
        fullName: 'Сидоров Дмитрий Петрович',
        competence: 'Мобильная разработка',
        category: 'Специалист',
        region: 'Новосибирск',
        university: 'НГУ',
        photo: 'https://via.placeholder.com/150/e74c3c/ffffff?text=СД',
        description: 'Разработчик мобильных приложений с 5-летним опытом'
      },
      {
        id: 4,
        fullName: 'Козлова Анна Владимировна',
        competence: 'Кулинарное искусство',
        category: 'Студент',
        region: 'Казань',
        university: 'КГАУ',
        photo: 'https://via.placeholder.com/150/9b59b6/ffffff?text=КА',
        description: 'Специализируется на молекулярной кухне'
      },
      {
        id: 5,
        fullName: 'Николаев Игорь Олегович',
        competence: '3D-моделирование',
        category: 'Школьник',
        region: 'Екатеринбург',
        university: 'Гимназия №9',
        photo: 'https://via.placeholder.com/150/1abc9c/ffffff?text=НИ',
        description: 'Создает модели для игр и архитектурной визуализации'
      },
      {
        id: 6,
        fullName: 'Федорова Елена Сергеевна',
        competence: 'Флористика',
        category: 'Специалист',
        region: 'Москва',
        university: 'РГАУ-МСХА',
        photo: 'https://via.placeholder.com/150/f39c12/ffffff?text=ФЕ',
        description: 'Владелица цветочного магазина, опыт работы 7 лет'
      },
      {
        id: 7,
        fullName: 'Волков Артем Алексеевич',
        competence: 'Системное администрирование',
        category: 'Студент',
        region: 'Санкт-Петербург',
        university: 'СПбГУ',
        photo: 'https://via.placeholder.com/150/34495e/ffffff?text=ВА',
        description: 'Администрирует серверы в крупной IT-компании'
      },
      {
        id: 8,
        fullName: 'Тихонова Ольга Дмитриевна',
        competence: 'Швейное дело',
        category: 'Специалист',
        region: 'Новосибирск',
        university: 'НГПУ',
        photo: 'https://via.placeholder.com/150/d35400/ffffff?text=ТО',
        description: 'Создает авторскую одежду, проводит мастер-классы'
      },
      {
        id: 9,
        fullName: 'Григорьев Павел Викторович',
        competence: 'Веб-разработка',
        category: 'Школьник',
        region: 'Казань',
        university: 'IT-лицей КФУ',
        photo: 'https://via.placeholder.com/150/3498db/ffffff?text=ГП',
        description: 'Разрабатывает веб-приложения на React'
      },
      {
        id: 10,
        fullName: 'Семенова Виктория Андреевна',
        competence: 'Графический дизайн',
        category: 'Студент',
        region: 'Екатеринбург',
        university: 'УрФУ',
        photo: 'https://via.placeholder.com/150/2ecc71/ffffff?text=СВ',
        description: 'Специализируется на UI/UX дизайне'
      }
    ];
    
    export const regions = ['Все', 'Москва', 'Санкт-Петербург', 'Новосибирск', 'Казань', 'Екатеринбург'];
    export const categoriesList = ['Все', 'Школьник', 'Студент', 'Специалист'];
    export const competenciesList = ['Все', 'Веб-разработка', 'Графический дизайн', 'Мобильная разработка', 'Кулинарное искусство', '3D-моделирование', 'Флористика', 'Системное администрирование', 'Швейное дело'];
  2. Создание компонента FilterPanel В папке src/components/ создайте файл FilterPanel.js:

    import React from 'react';
    import './FilterPanel.css';
    
    function FilterPanel({ filters, onFilterChange }) {
      return (
        <div className="filter-panel">
          <div className="filter-group">
            <label htmlFor="search">Поиск по имени:</label>
            <input
              id="search"
              type="text"
              value={filters.search}
              onChange={(e) => onFilterChange('search', e.target.value)}
              placeholder="Введите имя участника"
            />
          </div>
    
          <div className="filter-group">
            <label htmlFor="competence">Компетенция:</label>
            <select
              id="competence"
              value={filters.competence}
              onChange={(e) => onFilterChange('competence', e.target.value)}
            >
              <option value="Все">Все компетенции</option>
              {filters.competenceOptions && filters.competenceOptions.map((comp) => (
                <option key={comp} value={comp}>{comp}</option>
              ))}
            </select>
          </div>
    
          <div className="filter-group">
            <label htmlFor="category">Категория:</label>
            <select
              id="category"
              value={filters.category}
              onChange={(e) => onFilterChange('category', e.target.value)}
            >
              <option value="Все">Все категории</option>
              {filters.categoryOptions && filters.categoryOptions.map((cat) => (
                <option key={cat} value={cat}>{cat}</option>
              ))}
            </select>
          </div>
    
          <div className="filter-group">
            <label htmlFor="region">Регион:</label>
            <select
              id="region"
              value={filters.region}
              onChange={(e) => onFilterChange('region', e.target.value)}
            >
              <option value="Все">Все регионы</option>
              {filters.regionOptions && filters.regionOptions.map((reg) => (
                <option key={reg} value={reg}>{reg}</option>
              ))}
            </select>
          </div>
    
          <button
            className="reset-filters"
            onClick={() => onFilterChange('reset')}
          >
            Сбросить фильтры
          </button>
        </div>
      );
    }
    
    export default FilterPanel;
  3. Создание стилей для FilterPanel В папке src/components/ создайте файл FilterPanel.css:

    .filter-panel {
      background-color: white;
      padding: 1.5rem;
      border-radius: 10px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      margin-bottom: 2rem;
    }
    
    .filter-group {
      margin-bottom: 1.2rem;
    }
    
    .filter-group:last-child {
      margin-bottom: 1.5rem;
    }
    
    .filter-group label {
      display: block;
      margin-bottom: 0.5rem;
      font-weight: 500;
      color: #2c3e50;
    }
    
    .filter-group input,
    .filter-group select {
      width: 100%;
      padding: 0.7rem;
      border: 2px solid #ddd;
      border-radius: 5px;
      font-size: 1rem;
      transition: border-color 0.3s;
    }
    
    .filter-group input:focus,
    .filter-group select:focus {
      outline: none;
      border-color: #3498db;
      box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
    }
    
    .reset-filters {
      background-color: #95a5a6;
      color: white;
      border: none;
      padding: 0.7rem 1.5rem;
      border-radius: 5px;
      font-size: 0.9rem;
      cursor: pointer;
      transition: background-color 0.3s;
      width: 100%;
    }
    
    .reset-filters:hover {
      background-color: #7f8c8d;
    }
    
    @media (min-width: 768px) {
      .filter-panel {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        gap: 1.5rem;
        align-items: end;
      }
      
      .filter-group {
        margin-bottom: 0;
      }
      
      .reset-filters {
        grid-column: span 4;
      }
    }
    
    @media (min-width: 992px) {
      .filter-panel {
        grid-template-columns: 2fr repeat(3, 1fr) auto;
      }
      
      .reset-filters {
        grid-column: span 1;
        width: auto;
      }
    }
  4. Создание компонента ParticipantCard В папке src/components/ создайте файл ParticipantCard.js:

    import React from 'react';
    import './ParticipantCard.css';
    
    function ParticipantCard({ participant }) {
      return (
        <div className="participant-card">
          <div className="participant-photo">
            <img 
              src={participant.photo} 
              alt={`Фото ${participant.fullName}`}
              loading="lazy"
            />
            <div className="participant-category">{participant.category}</div>
          </div>
          
          <div className="participant-info">
            <h3 className="participant-name">{participant.fullName}</h3>
            
            <div className="participant-details">
              <div className="detail-item">
                <span className="detail-label">Компетенция:</span>
                <span className="detail-value">{participant.competence}</span>
              </div>
              
              <div className="detail-item">
                <span className="detail-label">Регион:</span>
                <span className="detail-value">{participant.region}</span>
              </div>
              
              <div className="detail-item">
                <span className="detail-label">Учебное заведение:</span>
                <span className="detail-value">{participant.university}</span>
              </div>
            </div>
            
            {participant.description && (
              <div className="participant-description">
                <p>{participant.description}</p>
              </div>
            )}
          </div>
        </div>
      );
    }
    
    export default ParticipantCard;
  5. Создание стилей для ParticipantCard В папке src/components/ создайте файл ParticipantCard.css:

    .participant-card {
      display: flex;
      background-color: white;
      border-radius: 10px;
      overflow: hidden;
      box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
      transition: transform 0.3s, box-shadow 0.3s;
    }
    
    .participant-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
    }
    
    .participant-photo {
      position: relative;
      flex-shrink: 0;
      width: 200px;
    }
    
    .participant-photo img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    
    .participant-category {
      position: absolute;
      top: 10px;
      right: 10px;
      background-color: #3498db;
      color: white;
      padding: 0.3rem 0.8rem;
      border-radius: 15px;
      font-size: 0.8rem;
      font-weight: 500;
    }
    
    .participant-info {
      flex: 1;
      padding: 1.5rem;
    }
    
    .participant-name {
      margin: 0 0 1rem 0;
      color: #2c3e50;
      font-size: 1.4rem;
    }
    
    .participant-details {
      margin-bottom: 1rem;
    }
    
    .detail-item {
      margin-bottom: 0.5rem;
      display: flex;
      align-items: flex-start;
    }
    
    .detail-label {
      font-weight: 500;
      color: #7f8c8d;
      min-width: 150px;
    }
    
    .detail-value {
      color: #2c3e50;
      flex: 1;
    }
    
    .participant-description {
      padding-top: 1rem;
      border-top: 1px solid #eee;
    }
    
    .participant-description p {
      margin: 0;
      color: #34495e;
      line-height: 1.6;
    }
    
    @media (max-width: 768px) {
      .participant-card {
        flex-direction: column;
      }
      
      .participant-photo {
        width: 100%;
        height: 200px;
      }
      
      .participant-info {
        padding: 1rem;
      }
      
      .participant-name {
        font-size: 1.2rem;
      }
      
      .detail-item {
        flex-direction: column;
      }
      
      .detail-label {
        min-width: auto;
        margin-bottom: 0.2rem;
      }
    }
  6. Обновление страницы Participants Обновите src/pages/Participants.js:

    import React, { useState, useMemo } from 'react';
    import { participants, regions, categoriesList, competenciesList } from '../data/participants';
    import FilterPanel from '../components/FilterPanel';
    import ParticipantCard from '../components/ParticipantCard';
    import './Participants.css';
    
    function Participants() {
      const [filters, setFilters] = useState({
        search: '',
        competence: 'Все',
        category: 'Все',
        region: 'Все',
        competenceOptions: competenciesList,
        categoryOptions: categoriesList,
        regionOptions: regions
      });
    
      const handleFilterChange = (filterName, value) => {
        if (filterName === 'reset') {
          setFilters({
            search: '',
            competence: 'Все',
            category: 'Все',
            region: 'Все',
            competenceOptions: competenciesList,
            categoryOptions: categoriesList,
            regionOptions: regions
          });
        } else {
          setFilters(prev => ({
            ...prev,
            [filterName]: value
          }));
        }
      };
    
      // Фильтрация участников
      const filteredParticipants = useMemo(() => {
        return participants.filter((participant) => {
          // Фильтр по поиску
          const matchesSearch = filters.search === '' || 
            participant.fullName.toLowerCase().includes(filters.search.toLowerCase());
          
          // Фильтр по компетенции
          const matchesCompetence = filters.competence === 'Все' || 
            participant.competence === filters.competence;
          
          // Фильтр по категории
          const matchesCategory = filters.category === 'Все' || 
            participant.category === filters.category;
          
          // Фильтр по региону
          const matchesRegion = filters.region === 'Все' || 
            participant.region === filters.region;
          
          return matchesSearch && matchesCompetence && matchesCategory && matchesRegion;
        });
      }, [filters]);
    
      // Статистика
      const stats = useMemo(() => {
        const total = participants.length;
        const filtered = filteredParticipants.length;
        const byCategory = {};
        const byRegion = {};
        
        participants.forEach(p => {
          byCategory[p.category] = (byCategory[p.category] || 0) + 1;
          byRegion[p.region] = (byRegion[p.region] || 0) + 1;
        });
        
        return { total, filtered, byCategory, byRegion };
      }, [filteredParticipants]);
    
      return (
        <div className="participants-page">
          <h1>Участники фестиваля</h1>
          
          {/* Статистика */}
          <div className="participants-stats">
            <div className="stat-card">
              <div className="stat-value">{stats.total}</div>
              <div className="stat-label">Всего участников</div>
            </div>
            <div className="stat-card">
              <div className="stat-value">{stats.filtered}</div>
              <div className="stat-label">Найдено</div>
            </div>
            <div className="stat-card">
              <div className="stat-value">{Object.keys(stats.byCategory).length}</div>
              <div className="stat-label">Категорий</div>
            </div>
            <div className="stat-card">
              <div className="stat-value">{Object.keys(stats.byRegion).length}</div>
              <div className="stat-label">Регионов</div>
            </div>
          </div>
          
          {/* Панель фильтров */}
          <FilterPanel 
            filters={filters}
            onFilterChange={handleFilterChange}
          />
          
          {/* Результаты поиска */}
          <div className="results-info">
            <p>
              Найдено участников: <strong>{filteredParticipants.length}</strong>
              {filters.search && ` по запросу "${filters.search}"`}
              {filters.competence !== 'Все' && ` в компетенции "${filters.competence}"`}
            </p>
          </div>
          
          {/* Список участников */}
          <div className="participants-list">
            {filteredParticipants.length > 0 ? (
              filteredParticipants.map((participant) => (
                <ParticipantCard 
                  key={participant.id} 
                  participant={participant}
                />
              ))
            ) : (
              <div className="no-results">
                <div className="no-results-icon">😔</div>
                <h3>Участники не найдены</h3>
                <p>Попробуйте изменить параметры поиска или сбросить фильтры</p>
                <button 
                  className="reset-btn"
                  onClick={() => handleFilterChange('reset')}
                >
                  Сбросить все фильтры
                </button>
              </div>
            )}
          </div>
          
          {/* Информация о распределении */}
          {filteredParticipants.length > 0 && (
            <div className="distribution-info">
              <h3>Распределение участников</h3>
              <div className="distribution-grid">
                <div className="distribution-item">
                  <h4>По категориям:</h4>
                  <ul>
                    {Object.entries(stats.byCategory).map(([category, count]) => (
                      <li key={category}>
                        <span className="dist-category">{category}:</span>
                        <span className="dist-count">{count}</span>
                      </li>
                    ))}
                  </ul>
                </div>
                <div className="distribution-item">
                  <h4>По регионам:</h4>
                  <ul>
                    {Object.entries(stats.byRegion).map(([region, count]) => (
                      <li key={region}>
                        <span className="dist-category">{region}:</span>
                        <span className="dist-count">{count}</span>
                      </li>
                    ))}
                  </ul>
                </div>
              </div>
            </div>
          )}
        </div>
      );
    }
    
    export default Participants;
  7. Создание стилей для Participants В папке src/pages/ создайте файл Participants.css:

    .participants-page {
      max-width: 1200px;
      margin: 0 auto;
    }
    
    .participants-page h1 {
      text-align: center;
      color: #2c3e50;
      margin-bottom: 2rem;
    }
    
    .participants-stats {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 1rem;
      margin-bottom: 2rem;
    }
    
    .stat-card {
      background-color: white;
      border-radius: 8px;
      padding: 1.5rem;
      text-align: center;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
      border-top: 4px solid #3498db;
    }
    
    .stat-value {
      font-size: 2.5rem;
      font-weight: bold;
      color: #2c3e50;
      margin-bottom: 0.5rem;
    }
    
    .stat-label {
      color: #7f8c8d;
      font-size: 0.9rem;
      text-transform: uppercase;
      letter-spacing: 1px;
    }
    
    .results-info {
      background-color: #f8f9fa;
      padding: 1rem 1.5rem;
      border-radius: 8px;
      margin-bottom: 2rem;
      border-left: 4px solid #3498db;
    }
    
    .results-info p {
      margin: 0;
      color: #34495e;
    }
    
    .results-info strong {
      color: #2c3e50;
    }
    
    .participants-list {
      display: flex;
      flex-direction: column;
      gap: 1.5rem;
      margin-bottom: 3rem;
    }
    
    .no-results {
      text-align: center;
      padding: 4rem 2rem;
      background-color: #f8f9fa;
      border-radius: 10px;
      border: 2px dashed #ddd;
    }
    
    .no-results-icon {
      font-size: 3rem;
      margin-bottom: 1rem;
    }
    
    .no-results h3 {
      color: #2c3e50;
      margin-bottom: 0.5rem;
    }
    
    .no-results p {
      color: #7f8c8d;
      margin-bottom: 1.5rem;
    }
    
    .reset-btn {
      background-color: #3498db;
      color: white;
      border: none;
      padding: 0.8rem 2rem;
      border-radius: 5px;
      font-size: 1rem;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    .reset-btn:hover {
      background-color: #2980b9;
    }
    
    .distribution-info {
      background-color: white;
      padding: 2rem;
      border-radius: 10px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }
    
    .distribution-info h3 {
      color: #2c3e50;
      margin-bottom: 1.5rem;
      text-align: center;
    }
    
    .distribution-grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 2rem;
    }
    
    .distribution-item h4 {
      color: #3498db;
      margin-bottom: 1rem;
      font-size: 1.1rem;
    }
    
    .distribution-item ul {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .distribution-item li {
      display: flex;
      justify-content: space-between;
      padding: 0.5rem 0;
      border-bottom: 1px solid #eee;
    }
    
    .distribution-item li:last-child {
      border-bottom: none;
    }
    
    .dist-category {
      color: #2c3e50;
    }
    
    .dist-count {
      background-color: #3498db;
      color: white;
      padding: 0.2rem 0.8rem;
      border-radius: 12px;
      font-size: 0.9rem;
      font-weight: 500;
    }
    
    @media (max-width: 768px) {
      .participants-stats {
        grid-template-columns: repeat(2, 1fr);
      }
      
      .stat-value {
        font-size: 2rem;
      }
      
      .distribution-grid {
        grid-template-columns: 1fr;
        gap: 1.5rem;
      }
      
      .distribution-info {
        padding: 1.5rem;
      }
    }
    
    @media (max-width: 480px) {
      .participants-stats {
        grid-template-columns: 1fr;
      }
    }
  8. Добавление кнопки "Показать еще" для пагинации (опционально) Добавьте в src/pages/Participants.js:

    // Добавьте состояние для пагинации
    const [visibleCount, setVisibleCount] = useState(6);
    
    // Обновите отображение участников
    const displayedParticipants = filteredParticipants.slice(0, visibleCount);
    
    // Добавьте кнопку "Показать еще" после списка
    {filteredParticipants.length > visibleCount && (
      <div className="load-more">
        <button
          className="load-more-btn"
          onClick={() => setVisibleCount(prev => prev + 6)}
        >
          Показать еще ({filteredParticipants.length - visibleCount})
        </button>
      </div>
    )}
    
    // Добавьте стили в Participants.css
    .load-more {
      text-align: center;
      margin: 2rem 0;
    }
    
    .load-more-btn {
      background-color: #27ae60;
      color: white;
      border: none;
      padding: 0.8rem 2rem;
      border-radius: 5px;
      font-size: 1rem;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    .load-more-btn:hover {
      background-color: #219653;
    }

Проверка:

  1. Перейдите на страницу /participants
  2. Проверьте работу поиска по имени
  3. Проверьте фильтры по компетенции, категории и региону
  4. Убедитесь, что все фильтры работают совместно
  5. Проверьте кнопку "Сбросить фильтры"
  6. Проверьте отображение карточек участников
  7. Проверьте адаптивность на мобильных устройствах
  8. Проверьте отображение статистики
  9. Проверьте сообщение при отсутствии результатов

About

5

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors