Калькулятор валют с курсами
📌 Функционал
- Ввод данных
- Сумма для конвертации (input)
- Валюта «из» (select, например USD)
- Валюта «в» (select, например EUR)
- Получение курса
- При клике «Конвертировать» — запрашиваем курс
- Выводим результат на экран, например:
100 USD → 92 EUR
- История / избранные пары
- Отображаем список последних 10 конвертаций
- Храним этот список в localStorage
- Добавляем кнопку «Очистить историю»
- Сортировка истории (по дате, сумме)
- Обработка состояний запроса
- Loader во время загрузки данных по сети
- Если API не отвечает/Нет интернета → показываем ошибку
API
- https://api.exchangeratesapi.io
✅ Чеклист для ревью
Корректно работает ввод суммы, выбор валют, кнопка конвертации
Добавляет результаты в localStorage
Ограничивает историю 10 записями
Чистит историю кнопкой «Clear History»
Показывает ошибку при сбое API
Показывает loader при загрузке данных
Виджет авторизации
📌 Функционал
Необходимо написать виджет авторизации.
Реализовать api-сервер со следующими endpoint:
POST /v1/auth/sign-in Params: { login: string, passwrod: string } Response: { authToken: string } POST /v2/auth/sign-in Params: { login: string, passwrod: string } Response: authToken in cookies GET /users/me Params: authToken in cookies Response: { login: string }
Реализовать форму на клиенте с переключателем формата регистрации (v1, v2).
После заполнения формы должен показываться блок с login-ом пользователя.
Выписывать настоящие токены не нужно. Придумайте рандомный токен и валидируйте его хардкодом на сервере, чтобы не усложнять себе задание.
✅ Чеклист для ревью
API реализует возвращение токена в cookies и устанавливает на клиент
API реализует возвращение токена в body
На фронте можно переключиться между версиями авторизации
Многошаговая форма
📌 Функционал
Нужно реализовать многошаговый конструктор формы, где структура формы полностью описывается конфигом, получаемым с бэкенда.
Технический стек
- React
- TypeScript (must have)
- Любой менеджер состояния (React контекст, Zustand, Redux, jotai и т.п.).
- Без UI-фреймворков типа MUI/Antd — стили можно простые, через CSS/SCSS/Styled Components/…
Функциональные требования
- Многошаг
- Форма разбита на шаги (pages).
- Есть навигация “Назад/Далее”.
- Нельзя перейти дальше, пока текущий шаг невалиден.
- Отображать прогресс (например, “Шаг 2 из 4” или прогресс-бар).
- Конфиг-драйв форма (SDUI/BDUI)
- Структура формы не захардкожена в JSX, а описана в JSON-конфиге, который фронт получает с API.
- На основе конфига рендерятся:
- Заголовки шагов
- Поля формы
- Правила валидации
- Условия видимости полей
- Типы полей (минимум такие)
text— обычный текстовый инпутnumberselect— выпадающий списокcheckboxradiotextarea
- Валидация
- Поддержать в конфиге:
requiredminLength,maxLength(для текстовых)min,max(для number)pattern(RegExp строкой)- Ошибки показывать пользователю под полем или рядом.
- Условная видимость полей (conditional logic)
- В конфиге поле может иметь условие вида “показывать только если другое поле имеет такое-то значение”.
- Например:
- если
hasCar === true, показываем поля о машине; - если
employmentStatus === "self_employed", показываем блок полей о бизнесе.
- Загрузка конфига и отправка результата
- При старте приложение запрашивает конфиг по API
GET /api/form. - После прохождения всех шагов и успешной валидации — отправить данные формы на
POST /api/form. - Обработать:
- состояние загрузки конфига
- ошибку при загрузке
- состояние отправки формы, успех/ошибка
- UX
- Кнопка “Назад” недоступна на первом шаге.
- Кнопка “Далее” дизейблится или показывает ошибки, если есть невалидные обязательные поля.
- При ошибке отправки формы показать пользователю сообщение.
- Поддержка сохранения прогресса (например, в
localStorage). - Integration тесты на ключевые части логики (валидация, условная видимость).
Формат конфига
Пример ответа
GET /api/form:{ "title": "Регистрация пользователя", "pages": [ { "id": "personal", "title": "Личные данные", "fields": [ { "id": "firstName", "type": "text", "label": "Имя", "placeholder": "Введите имя", "validation": { "required": true, "minLength": 2, "maxLength": 30 } }, { "id": "age", "type": "number", "label": "Возраст", "validation": { "required": true, "min": 18, "max": 120 } }, { "id": "employmentStatus", "type": "radio", "label": "Занятость", "options": [ { "value": "employed", "label": "Работаю по найму" }, { "value": "self_employed", "label": "Самозанятый/бизнес" }, { "value": "unemployed", "label": "Не работаю" } ], "validation": { "required": true } } ] }, { "id": "business", "title": "Данные о бизнесе", "fields": [ { "id": "companyName", "type": "text", "label": "Название компании", "visibilityCondition": { "fieldId": "employmentStatus", "operator": "eq", "value": "self_employed" }, "validation": { "required": true, "minLength": 2 } }, { "id": "employeesCount", "type": "number", "label": "Количество сотрудников", "visibilityCondition": { "fieldId": "employmentStatus", "operator": "eq", "value": "self_employed" } } ] }, { "id": "other", "title": "Прочее", "fields": [ { "id": "hasCar", "type": "checkbox", "label": "Есть ли у вас авто?" }, { "id": "carModel", "type": "text", "label": "Марка автомобиля", "visibilityCondition": { "fieldId": "hasCar", "operator": "eq", "value": true }, "validation": { "required": true } } ] } ] }