Домашнє завдання #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
Як здати
- Це — окремий публічний репозиторій
README.md: що за продукт, який API використано, як запустити, які env-змінні потрібні (і що їх взяти можна після реєстрації на сайті API)- Якщо є ключ:
.env— НЕ комітити, у репозиторії —.env.example - Мінімум 5 комітів з осмисленими повідомленнями
- Скріншоти в
screenshots/:desktop.png(сітка головної),mobile.png,interaction.png(сторінка деталей або Favorites із збереженими елементами) - Здай посиланням на репозиторій
Критерії оцінювання
| Критерій | Тип | Бали |
|---|---|---|
| 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
const [favorites, setFavorites] = useState<Item[]>(() => JSON.parse(localStorage.getItem('favorites') ?? '[]'))useEffect(() => localStorage.setItem('favorites', JSON.stringify(favorites)), [favorites])addFavorite: item => setFavorites(prev => [...prev, item]);removeFavorite: id => setFavorites(prev => prev.filter(i => i.id !== id));isFavorite: id => favorites.some(i => i.id === id)- Кнопка-серце:
isFavorite(item.id) ? removeFavorite(item.id) : addFavorite(item)— і не забудьevent.stopPropagation(), щоб клік по серцю не відкривав деталі - Лічильник у Header:
favorites.lengthчерезuseFavorites()
Що далі
Цей каталог — серце твого портфоліо. У ДЗ #25 ти даси йому власний бекенд на Next.js (CRUD своїх записів, а не тільки чужий API), а в ДЗ #26 він стане головним кейсом у секції проєктів твого сайту. Не видаляй цей код!