Урок 20. Домашнее задание: форма профиля
⚡ Кратко
Собрать форму профиля с валидацией по схеме: текстовые поля, выбор, переключатели, загрузка аватара, обработка ошибок и состояние отправки.
Задание
В проекте курса сделайте страницу «Профиль» с формой:
- имя и фамилия (обязательные, от двух символов);
- почта (валидный адрес,
autocomplete); - должность (необязательное поле);
- часовой пояс —
Select; - «О себе» —
Textareaс ограничением 300 символов и счётчиком; - переключатели уведомлений (
Switch) в горизонтальных полях; - кнопка сохранения с состоянием загрузки и сообщением об успехе.
Подготовка окружения
cd tailwind-course
npx shadcn@latest add field input textarea select switch button
npm install react-hook-form zod @hookform/resolvers
npm run dev
Пошаговое решение
Шаг 1. Схема
📄 src/lib/schemas.ts
// src/lib/schemas.ts
import { z } from "zod"
export const profileSchema = z.object({
firstName: z.string().min(2, "Минимум два символа"),
lastName: z.string().min(2, "Минимум два символа"),
email: z.email("Введите корректный адрес"),
position: z.string().optional(),
timezone: z.string().min(1, "Выберите часовой пояс"),
about: z.string().max(300, "Не более 300 символов").optional(),
emailNotifications: z.boolean(),
weeklyDigest: z.boolean(),
})
export type ProfileValues = z.infer<typeof profileSchema>
Шаг 2. Разметка полей
Каждое поле — Field с подписью, при необходимости
подсказкой и FieldError. Группы разделите
FieldSet с FieldLegend: «Личные данные»
и «Уведомления».
Шаг 3. Счётчик символов
const about = watch("about") ?? ""
<FieldDescription>
{about.length} / 300 символов
</FieldDescription>
Счётчик — не украшение: без него ограничение обнаруживается только после отправки.
Шаг 4. Отправка и результат
На время запроса блокируйте кнопку, после успеха покажите сообщение и переведите на него фокус. При ошибке сервера покажите её в форме, а не в консоли.
Как проверить
- Форма проходится только клавиатурой от начала до конца.
- Каждая ошибка объясняет, что сделать.
- Менеджер паролей корректно подставляет почту.
- Двойной клик по кнопке не отправляет форму дважды.
- Форма читаема в тёмной теме.
Ориентир
<form class="w-full max-w-lg space-y-5 rounded-xl bg-card p-4 text-card-foreground ring-1 ring-border">
<fieldset class="space-y-4">
<legend class="text-sm font-medium">Личные данные</legend>
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-1.5">
<label class="text-sm font-medium" for="hw-first">Имя</label>
<input id="hw-first" value="Ирина" class="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50">
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium" for="hw-last">Фамилия</label>
<input id="hw-last" value="С" aria-invalid="true" aria-describedby="hw-last-err" class="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20">
<div id="hw-last-err" role="alert" class="text-sm text-destructive">Минимум два символа</div>
</div>
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium" for="hw-about">О себе</label>
<textarea id="hw-about" rows="3" class="w-full rounded-lg border border-input bg-transparent px-2.5 py-1.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50">Продуктовый аналитик</textarea>
<p class="text-sm text-muted-foreground">21 / 300 символов</p>
</div>
</fieldset>
<fieldset class="space-y-3">
<legend class="text-sm font-medium">Уведомления</legend>
<label class="flex items-center justify-between gap-4">
<span class="text-sm">Письма о новых отчётах</span>
<input type="checkbox" checked class="peer sr-only">
<span class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full bg-input transition-colors peer-checked:bg-primary">
<span class="ml-0.5 size-4 rounded-full bg-background transition-transform peer-checked:translate-x-4"></span>
</span>
</label>
</fieldset>
<div class="flex justify-end gap-2">
<button type="button" class="inline-flex h-8 items-center justify-center rounded-lg px-3 text-sm font-medium hover:bg-muted">Отмена</button>
<button type="button" class="inline-flex h-8 items-center justify-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground">Сохранить</button>
</div>
</form>