Домашнє завдання #21: Pokedex — каталог покемонів
Твій перший справжній React-застосунок: каталог 150 покемонів з живим пошуком і фільтрами. Дані — з безкоштовного PokeAPI, без реєстрації та ключів. Тут ти вперше збереш разом усе: компоненти, props, useState, списки й умовний рендеринг.
Підготовка
npm create vite@latest pokedex -- --template react-ts
cd pokedex
npm install
npm run dev
Завдання
1. Компонент PokemonCard
Перевикористовуваний компонент картки з таким контрактом props:
interface PokemonCardProps {
name: string
image: string
types: string[]
id: number
}
Картка показує: зображення, ім'я (з великої літери), номер у форматі #001, і типи — як кольорові бейджики.
2. Завантаження даних
Список перших 150 покемонів: https://pokeapi.co/api/v2/pokemon?limit=150. Для кожного — окремий запит по деталі (зображення, типи): https://pokeapi.co/api/v2/pokemon/1, .../2 і т.д.
Завантаж усе один раз при першому рендері (useEffect + Promise.all). Поки дані вантажаться — компонент Loader замість сітки.
3. Пошук за ім'ям
Поле введення зверху сторінки. Список фільтрується при кожному введеному символі — без кнопки «Шукати». Пошук нечутливий до регістру: PIKA знаходить pikachu.
4. Фільтр за типом
Ряд кнопок-фільтрів з типами, які реально є в даних (fire, water, grass, electric, psychic, normal...):
- Клік на кнопку — лишаються тільки покемони цього типу
- Повторний клік на активну кнопку або кнопка «Всі» — скидає фільтр
- Активна кнопка візуально виділена
- Пошук і фільтр типу працюють разом: «char» + fire → charmander, charmeleon, charizard
5. Порожній стан
Якщо під фільтри не потрапив ніхто — повідомлення «Покемонів не знайдено» і кнопка «Скинути фільтри», яка очищає і пошук, і тип.
6. Стилізація
- Картки в CSS Grid: 4 колонки на десктопі → 2 на планшеті → 1-2 на мобільному
- Кожен тип — свій колір бейджика (fire — червоний, water — синій, grass — зелений...)
- Hover-ефект на картці: тінь або легке збільшення
Структура проєкту
src/
├── components/
│ ├── PokemonCard.tsx
│ ├── SearchBar.tsx
│ ├── TypeFilter.tsx
│ └── Loader.tsx
├── App.tsx
└── main.tsx
Як здати
- Це — окремий публічний репозиторій (не папка в
my-web-course: у React-проєкту своїnode_modules, білди та.gitignore) - У корені —
README.md: 2-3 речення про проєкт + як запустити (npm install,npm run dev) - Мінімум 5 комітів з осмисленими повідомленнями — наприклад: setup, картка, завантаження даних, пошук, фільтри
- Скріншоти в папці
screenshots/:desktop.png(~1440px) — сітка карток з кольоровими бейджами типівmobile.png(~390px) — 1-2 колонкиinteraction.png— стан пошуку з відфільтрованим списком або порожній стан
- Ключів тут немає, але звичка залишається:
.env— ніколи не в git - Здай посиланням на репозиторій
Критерії оцінювання
| Критерій | Тип | Бали |
|---|---|---|
PokemonCard: компонент з props за контрактом, ім'я з великої, номер #001, бейджики типів | [код] | 15 |
Завантаження 150 покемонів: useEffect + Promise.all, один раз при старті | [код] | 15 |
| Loader показується під час завантаження і зникає після | [поведінка] | 10 |
| Пошук у реальному часі, нечутливий до регістру («PIKA» → pikachu) | [поведінка] | 15 |
| Фільтр за типом: toggle, кнопка «Всі», активна кнопка виділена, працює разом з пошуком | [поведінка] | 15 |
| Порожній стан: повідомлення + «Скинути фільтри» очищає обидва фільтри | [поведінка] | 10 |
| Скріншоти: responsive grid 4→2→1, кольорові бейджики, hover/стан пошуку | [скрін] | 15 |
| Якість коду | [код] | 5 |
| Разом | 100 | |
| Бонус: клік на картку розкриває stats (HP/Attack/Defense) — модалка або розгортка | [поведінка] | +10 |
| Бонус: сортування за ім'ям/номером + лічильник «Знайдено 23 з 150» | [поведінка] | +10 |
Підказки
Рівень 1: напрямок
Тобі потрібно всього три стани: масив покемонів, текст пошуку, обраний тип. Відфільтрований список — не стан, а звичайна змінна, яку ти обчислюєш прямо в тілі компонента перед return. Коли state змінюється — React сам перерендерить список.
Рівень 2: які інструменти
useEffect(() => {...}, []) з порожнім масивом залежностей — виконається один раз. Усередині: fetch списку → з відповіді взяти масив → зробити з нього масив промісів детальних запитів → Promise.all. Для фільтрації: Array.filter + String.includes + toLowerCase. Умовний рендеринг: тернарник для Loader, && або length === 0 для порожнього стану. Активна кнопка — порівняй значення кнопки зі станом обраного типу і додай CSS-клас умовно.
Рівень 3: псевдокод
const [pokemons, setPokemons] = useState<Pokemon[]>([]), плюсloading,search,selectedType- В
useEffect: fetch списку →results.map(r => fetch(r.url).then(res => res.json()))→Promise.all→setPokemons,setLoading(false) - Перед return:
filtered = pokemons.filter(за ім'ям).filter(за типом, якщо тип обрано) - Клік по кнопці типу: якщо
selectedType === type→setSelectedType(null), інакше →setSelectedType(type) - Рендер:
loading ? <Loader/> : filtered.length === 0 ? <порожній стан/> : сітка filtered.map(...)— не забудьkey={pokemon.id}
Що далі
Патерн «список + пошук + фільтри» — це 80% усіх інтерфейсів, які ти будеш робити в житті. У ДЗ #23 ти повториш його вже для свого каталогу — з routing, улюбленими та справжнім API під твою тему.