Вивчай
Домашнє завдання #23 · React: routing, useEffect, Context, робота з API
100 балів+20 бонусintermediate

Домашнє завдання #23: Твій каталог у великому світі

Фінальний проєкт React-блоку. Досі твій продукт (з ДЗ #5) жив на власних даних. Час підключити його до великого світу: побудуй багатосторінковий застосунок на свою тему, який шукає дані у справжньому публічному API — так само, як це роблять Letterboxd, Goodreads чи Backloggd.

Що ти вже маєш

  • Тема продукту та бренд — з ДЗ #5
  • Досвід опису моделей інтерфейсами — з ДЗ #19: тут опишеш модель свого домену і типи відповідей API
  • Патерн «список + пошук + фільтри» — з ДЗ #21
  • ThemeContext — з ДЗ #22 (стане бонусом)

Обери API під свою тему

Твоя темаAPIКлюч
ФільмиTMDBБезкоштовна реєстрація
ІгриRAWGБезкоштовна реєстрація
КнигиOpen LibraryБез ключа
МузикаiTunes Search APIБез ключа
РецептиTheMealDBБез ключа

Якщо для твоєї теми публічного API немає (кросівки, подорожі...) — бери TMDB як дефолт і роби фільмотеку: механіка однакова, а навичка та сама. У прикладах нижче — фільмотека FilmShelf на TMDB.

Порада

Якщо API вимагає ключ — зберігай його у .env: VITE_API_KEY=твій_ключ, доступ через import.meta.env.VITE_API_KEY. .env — у .gitignore, а в репозиторій поклади .env.example з назвами змінних без значень.


Підготовка

npm create vite@latest my-catalog -- --template react-ts
cd my-catalog
npm install react-router-dom
npm run dev

Завдання

1. Сторінки та routing

/              → HomePage      (стартовий список + пошук)
/item/:id      → ItemPage      (деталі елемента)
/favorites     → FavoritesPage (збережене)
*              → NotFound      (404)

Layout зі спільним Header (логотип-бренд, навігація, лічильник улюблених) та Footer. Назви маршрутів можеш адаптувати під тему (/movie/:id, /game/:id) — головне, щоб сторінок було чотири.

2. Головна сторінка

  • При завантаженні — стартовий список: «популярне» з API, а якщо API такого не має — результати запиту за темою (напр. для Open Library — пошук за улюбленим жанром)
  • Поле пошуку: введення тексту → запит до API (по Enter або з debounce)
  • Індикатор завантаження під час запиту
  • Якщо API впав або мережі немає — зрозуміле повідомлення про помилку, не білий екран

3. Картка елемента (ItemCard)

  • Зображення (постер/обкладинка) або заглушка, якщо API його не віддав
  • Назва, рік (або автор/виконавець — що є у твоєму домені)
  • Рейтинг, якщо API його має: число з кольором (зелений > 7, жовтий > 5, червоний нижче)
  • Кнопка-серце «В улюблені» — toggle прямо з картки
  • Клік на картку → сторінка деталей

4. Сторінка деталей

  • useParams + useEffect: завантажити повну інформацію за id
  • Велике зображення, назва, опис, категорії/жанри бейджиками
  • Кнопка «В улюблені» та кнопка «Назад» (useNavigate)

5. FavoritesContext

  • favorites: Item[], методи addFavorite(item), removeFavorite(id), isFavorite(id)
  • Збереження в localStorage — улюблені переживають перезавантаження
  • У Header — живий лічильник: «Улюблені (3)»

6. Сторінка Favorites

  • Збережені елементи тими ж ItemCard
  • Порожній стан: «Тут поки порожньо» + посилання на головну
  • Кнопка «Очистити все»

Структура проєкту

src/
├── components/    # ItemCard, SearchBar, Loader, Layout, Rating
├── pages/         # HomePage, ItemPage, FavoritesPage, NotFound
├── context/       # FavoritesContext.tsx
├── types/         # item.ts — модель домену + типи відповідей API
├── App.tsx
└── main.tsx

Як здати

  1. Це — окремий публічний репозиторій
  2. README.md: що за продукт, який API використано, як запустити, які env-змінні потрібні (і що їх взяти можна після реєстрації на сайті API)
  3. Якщо є ключ: .envНЕ комітити, у репозиторії — .env.example
  4. Мінімум 5 комітів з осмисленими повідомленнями
  5. Скріншоти в screenshots/: desktop.png (сітка головної), mobile.png, interaction.png (сторінка деталей або Favorites із збереженими елементами)
  6. Здай посиланням на репозиторій

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

КритерійТипБали
Routing: 4 сторінки, Layout з Header/Footer, 404 для невідомих шляхів[код]10
Головна: стартовий список з API + робочий пошук[поведінка]15
ItemCard: props, заглушка зображення, клік веде на деталі[код]10
Сторінка деталей: useParams + useEffect, опис, бейджики, «Назад»[поведінка]10
FavoritesContext: add/remove/isFavorite, localStorage, лічильник у Header[код]15
Favorites: список, порожній стан з посиланням, «Очистити все»[поведінка]10
Loading-стан під час запитів + повідомлення при помилці API[поведінка]10
Типи: модель домену + типізовані відповіді API, без any[код]5
Скріншоти: responsive сітка, кольоровий рейтинг/бейджики, сторінка деталей[скрін]10
Якість коду[код]5
Разом100
Бонус: пагінація результатів (кнопки або «Завантажити ще»)[поведінка]+5
Бонус: фільтр за жанром/категорією з даних API[поведінка]+5
Бонус: темна/світла тема — перенеси ThemeContext з ДЗ #22[поведінка]+10

Підказки

Рівень 1: напрямок

Порядок збирання: routing з порожніми сторінками → головна зі списком → картка → деталі → улюблені. Кожен крок — робочий застосунок і окремий коміт. Відповідь API майже завжди «багатша», ніж тобі треба, — одразу перетвори її на свій компактний тип Item в одному місці (функція-маппер), і решта коду не залежатиме від формату API.

Рівень 2: які інструменти

createBrowserRouter або Routes/Route + Outlet для Layout; useParams дістає :id, useNavigate(-1) — «Назад». Запити — fetch в useEffect із залежністю [id] на сторінці деталей. Помилки — try/catch + перевірка response.ok, стани loading/error/data. FavoritesContext — той самий патерн provider + custom hook, що в ThemeContext з ДЗ #22; синхронізація з localStorage — useEffect із залежністю [favorites].

Рівень 3: псевдокод FavoritesContext
  1. const [favorites, setFavorites] = useState<Item[]>(() => JSON.parse(localStorage.getItem('favorites') ?? '[]'))
  2. useEffect(() => localStorage.setItem('favorites', JSON.stringify(favorites)), [favorites])
  3. addFavorite: item => setFavorites(prev => [...prev, item]); removeFavorite: id => setFavorites(prev => prev.filter(i => i.id !== id)); isFavorite: id => favorites.some(i => i.id === id)
  4. Кнопка-серце: isFavorite(item.id) ? removeFavorite(item.id) : addFavorite(item) — і не забудь event.stopPropagation(), щоб клік по серцю не відкривав деталі
  5. Лічильник у Header: favorites.length через useFavorites()

Що далі

Цей каталог — серце твого портфоліо. У ДЗ #25 ти даси йому власний бекенд на Next.js (CRUD своїх записів, а не тільки чужий API), а в ДЗ #26 він стане головним кейсом у секції проєктів твого сайту. Не видаляй цей код!