Вивчай
Домашнє завдання #21 · React: компоненти, props, state, списки
100 балів+20 бонусintermediate

Домашнє завдання #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

Як здати

  1. Це — окремий публічний репозиторій (не папка в my-web-course: у React-проєкту свої node_modules, білди та .gitignore)
  2. У корені — README.md: 2-3 речення про проєкт + як запустити (npm install, npm run dev)
  3. Мінімум 5 комітів з осмисленими повідомленнями — наприклад: setup, картка, завантаження даних, пошук, фільтри
  4. Скріншоти в папці screenshots/:
    • desktop.png (~1440px) — сітка карток з кольоровими бейджами типів
    • mobile.png (~390px) — 1-2 колонки
    • interaction.png — стан пошуку з відфільтрованим списком або порожній стан
  5. Ключів тут немає, але звичка залишається: .env — ніколи не в git
  6. Здай посиланням на репозиторій

Критерії оцінювання

КритерійТипБали
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: псевдокод
  1. const [pokemons, setPokemons] = useState<Pokemon[]>([]), плюс loading, search, selectedType
  2. В useEffect: fetch списку → results.map(r => fetch(r.url).then(res => res.json()))Promise.allsetPokemons, setLoading(false)
  3. Перед return: filtered = pokemons.filter(за ім'ям).filter(за типом, якщо тип обрано)
  4. Клік по кнопці типу: якщо selectedType === typesetSelectedType(null), інакше → setSelectedType(type)
  5. Рендер: loading ? <Loader/> : filtered.length === 0 ? <порожній стан/> : сітка filtered.map(...) — не забудь key={pokemon.id}

Що далі

Патерн «список + пошук + фільтри» — це 80% усіх інтерфейсів, які ти будеш робити в житті. У ДЗ #23 ти повториш його вже для свого каталогу — з routing, улюбленими та справжнім API під твою тему.