Цель: Создать страницу со списком участников, фильтрами по имени, компетенции, категории и региону.
-
Создание данных об участниках Создайте файл
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-моделирование', 'Флористика', 'Системное администрирование', 'Швейное дело'];
-
Создание компонента 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;
-
Создание стилей для 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; } }
-
Создание компонента 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;
-
Создание стилей для 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; } }
-
Обновление страницы 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;
-
Создание стилей для 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; } }
-
Добавление кнопки "Показать еще" для пагинации (опционально) Добавьте в
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; }
Проверка:
- Перейдите на страницу
/participants - Проверьте работу поиска по имени
- Проверьте фильтры по компетенции, категории и региону
- Убедитесь, что все фильтры работают совместно
- Проверьте кнопку "Сбросить фильтры"
- Проверьте отображение карточек участников
- Проверьте адаптивность на мобильных устройствах
- Проверьте отображение статистики
- Проверьте сообщение при отсутствии результатов