Домашнє завдання #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 символи |
| Обов'язкове, формат email | ||
| URL аватара | url | Необов'язкове |
| Посада | text | Обов'язкове |
| Рівень досвіду | select | Junior / 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
Як здати
- Це — окремий публічний репозиторій (не папка в
my-web-course) - У корені —
README.md: 2-3 речення про проєкт + як запустити (npm install,npm run dev) - Мінімум 5 комітів з осмисленими повідомленнями — наприклад: setup, ThemeContext, форма, валідація, прев'ю
- Скріншоти в папці
screenshots/:desktop.png(~1440px) — форма + прев'ю поручmobile.png(~390px)theme-dark.pngтаtheme-light.png— той самий екран в обох темах (або одинinteraction.pngз обома поруч)
.envтут не потрібен, але правило пам'ятай: ключі — ніколи не в git- Здай посиланням на репозиторій
Критерії оцінювання
| Критерій | Тип | Бали |
|---|---|---|
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: псевдокод
ThemeContext.tsx:const [theme, setTheme] = useState(() => localStorage.getItem('theme') ?? 'light');toggleперемикає і пише в localStorage; провайдер віддає{ theme, toggle }App.tsx:<ThemeProvider><div className={theme}>...— form state:const [form, setForm] = useState({ name: '', email: '', ... skills: [] }),const [errors, setErrors] = useState({}),const [submitted, setSubmitted] = useState(false)validateField(name, value)повертає текст помилки або порожньо;validateAll()проганяє всі поля і повертає об'єкт помилок- Кнопка:
disabled={Object.values(errors).some(Boolean) || обов'язкові порожні} handleSubmit:preventDefault→validateAll→ якщо чисто,setSubmitted(true)- Рендер:
submitted ? <ProfileCard large + кнопка Редагувати (setSubmitted(false))> : <форма + прев'ю>
Що далі
ThemeContext, який ти щойно написав, — багаторазовий: у ДЗ #23 його можна перенести у свій каталог як бонус, а патерн provider + custom hook знадобиться там для FavoritesContext. Не видаляй цей код!