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

Домашнє завдання #22: DevProfile — профіль розробника

Побудуй застосунок, де користувач заповнює форму — і в реальному часі бачить, як виглядає його картка розробника. Весь інтерфейс перемикається між темною і світлою темами через Context API. Це той самий патерн «форма + живе прев'ю», який ти бачив у генераторах резюме та біо-сторінок.


Підготовка

npm create vite@latest dev-profile -- --template react-ts
cd dev-profile
npm install
npm run dev

Завдання

1. ThemeContext

Файл src/context/ThemeContext.tsx:

  • Два значення теми: 'light' та 'dark'
  • Custom hook useTheme() для доступу з будь-якого компонента
  • Обрана тема зберігається в localStorage і відновлюється після перезавантаження сторінки
  • ThemeProvider обгортає весь застосунок

2. Кнопка перемикання теми

У header — кнопка-toggle: у світлій темі показує місяць (або текст «Темна тема»), у темній — сонце. При кліку весь інтерфейс змінює кольори: фон, текст, картка, поля форми.

3. Форма профілю

Усі поля — controlled (через useState):

ПолеТипВалідація
Ім'яtextОбов'язкове, мін. 2 символи
EmailemailОбов'язкове, формат email
URL аватараurlНеобов'язкове
ПосадаtextОбов'язкове
Рівень досвідуselectJunior / Middle / Senior
НавичкиcheckboxesМін. 1 обрана (React, TypeScript, CSS, Node.js, Git, Figma)
Про себеtextareaНеобов'язкове, макс. 300 символів

Поведінка валідації:

  • Помилка — червоним текстом під конкретним полем
  • Перевірка при blur (втраті фокусу) та при submit
  • Кнопка «Створити профіль» неактивна, поки форма невалідна

4. Прев'ю картки (real-time)

Поруч із формою (на мобільному — під нею) — компонент ProfileCard, який оновлюється з кожним символом:

┌─────────────────────────┐
│   [Avatar]              │
│   Олена Коваленко       │
│   Frontend Developer    │
│   ● Senior              │
│                         │
│   React  TypeScript CSS │
│                         │
│   "Люблю чистий код"    │
│   olena@example.com     │
└─────────────────────────┘
  • Немає аватара — заглушка з ініціалами (перші літери імені та прізвища)
  • Навички — кольорові бейджики
  • Стиль картки залежить від обраної теми

5. Стан після submit

Після успішного submit: форма зникає, показується фінальна картка у збільшеному вигляді, кнопка «Редагувати» повертає до форми із заповненими даними (нічого не втрачається).


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

src/
├── components/
│   ├── Header.tsx
│   ├── ProfileForm.tsx
│   ├── ProfileCard.tsx
│   └── ThemeToggle.tsx
├── context/
│   └── ThemeContext.tsx
├── App.tsx
└── main.tsx

Як здати

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

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

КритерійТипБали
ThemeContext: Provider, значення light/dark, custom hook useTheme()[код]15
Перемикання теми змінює весь інтерфейс, вибір переживає перезавантаження (localStorage)[поведінка]10
Форма: всі 7 полів з таблиці, всі controlled через useState[код]15
Валідація: помилки під полями, спрацьовує на blur і submit, кнопка disabled поки невалідно, мін. 1 навичка[поведінка]15
ProfileCard: оновлюється в реальному часі, заглушка з ініціалами без аватара[поведінка]15
Після submit: фінальна картка + «Редагувати» повертає форму з даними[поведінка]10
Скріншоти: обидві теми (контрастні, читабельні), бейджики навичок, mobile-версія[скрін]15
Якість коду[код]5
Разом100
Бонус: валідація email через regex + перевірка формату URL аватара[код]+5
Бонус: плавна анімація перемикання теми (CSS transition кольорів)[скрін]+5
Бонус: профіль зберігається в localStorage і відновлюється між сесіями[поведінка]+10

Підказки

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

Почни з ThemeContext — це каркас, у який вставиш усе інше. Найпростіший спосіб застосувати тему до всього: клас dark/light на кореневому div і CSS-змінні (--bg, --text, --card-bg), які перевизначаються в цих класах. Дані форми зручно тримати в одному об'єкті стану, а помилки — в окремому об'єкті з тими ж ключами.

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

createContext + useContext усередині useTheme(); у provider — useState з ініціалізацією з localStorage.getItem (лінива ініціалізація: useState(() => ...)). Для форми: один handleChange, що оновлює поле за event.target.name, спред попереднього стану. Checkboxes — масив рядків у стані: додати/прибрати через includes + filter. Валідація на blur — обробник onBlur, який валідує одне поле. Ініціали — split(' ') + перші символи слів.

Рівень 3: псевдокод
  1. ThemeContext.tsx: const [theme, setTheme] = useState(() => localStorage.getItem('theme') ?? 'light'); toggle перемикає і пише в localStorage; провайдер віддає { theme, toggle }
  2. App.tsx: <ThemeProvider><div className={theme}>... — form state: const [form, setForm] = useState({ name: '', email: '', ... skills: [] }), const [errors, setErrors] = useState({}), const [submitted, setSubmitted] = useState(false)
  3. validateField(name, value) повертає текст помилки або порожньо; validateAll() проганяє всі поля і повертає об'єкт помилок
  4. Кнопка: disabled={Object.values(errors).some(Boolean) || обов'язкові порожні}
  5. handleSubmit: preventDefaultvalidateAll → якщо чисто, setSubmitted(true)
  6. Рендер: submitted ? <ProfileCard large + кнопка Редагувати (setSubmitted(false))> : <форма + прев'ю>

Що далі

ThemeContext, який ти щойно написав, — багаторазовий: у ДЗ #23 його можна перенести у свій каталог як бонус, а патерн provider + custom hook знадобиться там для FavoritesContext. Не видаляй цей код!