Files
EventHubSpec/EventHubFrontSpec.md

213 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EventHub Client UI — Техническое задание
Клиентское Web SPA для end-user: владельцы календарей и участники. Администраторы работают в [EventHubFrontAdmin](EventHubFrontAdminSpec.md). Контракт API — [EventHubBackSpec.md](EventHubBackSpec.md) и маршруты `EventHubBack` (порт HTTP **8080**, WS **8081**).
## 1. Роли и режимы
Один пользовательский аккаунт; UI разделяется по контексту (не по JWT-роли админки):
| Режим | Описание | Ключевые возможности |
|-------|----------|----------------------|
| Участник | Поиск и запись на события коммерческих календарей | Поиск, просмотр календаря/события, запись, отмена своей записи, отзывы, жалобы, тикеты |
| Владелец | Управление своими календарями | CRUD календарей и событий, список заявок, confirm/decline, подписка |
Deep-link на календарь, владельцем которого является текущий пользователь, показывает owner-действия (редактирование, заявки).
Права на операции проверяет бэкенд; фронт скрывает недоступные действия по `owner_id` / ответу API.
## 2. Аутентификация и авторизация
Эндпоинты:
- `POST /v1/register` — регистрация (`email`, `password`); статус пользователя `pending` до верификации.
- `POST /v1/verify` — подтверждение email `{ token }`; после успеха у пользователя есть дефолтный personal-календарь.
- `POST /v1/login` — вход; ответ `{ token, refresh_token, user }`. Сессия `auth_session` с `client_type=web`.
- `POST /v1/refresh` — ротация пары токенов по `{ refresh_token }`.
- `GET /v1/user/me` — профиль текущего пользователя.
Реализация:
- JWT в `localStorage` под ключами, отличными от Admin UI (избежать коллизии при общем origin).
- Axios: `Authorization: Bearer <access>`; при 401 — single-flight refresh; при неудаче — редирект на `/login`.
- При старте приложения — `GET /v1/user/me` при наличии токена.
- Logout — только клиентский (очистка storage); серверного revoke нет.
- WebSocket: `ws://…:8081/ws?token=<access_jwt>`; subscribe/unsubscribe по `calendar_id`.
## 3. Технологический стек
- Язык: TypeScript
- Сборщик: Vite
- UI: React 19 + Tailwind CSS 4 + shadcn/Radix
- Типографика: Syne (display) + Manrope (UI)
- Состояние: Zustand (auth + mood + calendar context), TanStack React Query v5 (сервер)
- HTTP: Axios с интерсепторами
- Формы: React Hook Form + Zod
- Дата/время: dayjs (ru locale)
- i18n: react-i18next (ru, en)
- Роутинг: React Router 7
- WebSocket: нативный API с переподключением
- Контейнеризация: Docker, Nginx (прокси `/v1` → user API `:8080`, `/ws``:8081`)
## 3.1. UX / IA (calendar workspace, mobile-first)
- Главная сущность — **календарь**. Центр UI — виджет с видами **месяц** (default) / **неделя** / **день**.
- Главные вкладки: **Календарь** (`/`, `/c/:id`), **Найти** (`/search`), **Записи** (`/bookings`), **Ещё** (`/more`).
- Контекст виджета: свой календарь (селектор) или чужой (browse после поиска). `personal` чужой — только просмотр; `commercial` — запись на событие.
- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель. Действия зависят от роли (owner / participant).
- Без выбранного события — панель «О календаре» (описание, title/meta, рейтинг, отзывы).
Форма отзыва на календарь/событие — только при confirmed booking; жалоба доступна без записи.
На чужом календаре — CTA **«Отслеживать» / Follow** (не путать с платной «Подписка»);
follow не открывает форму отзыва.
- На чужом `/c/:id` — CTA «Мой календарь» и таб Calendar не `aria-current` (intent `browse`).
- Список отслеживаемых: `/following` (из «Ещё»).
- Открытие события — `navigate` push (не `replace`), чтобы Back возвращал к календарю.
- Owner: WS subscribe на активный календарь; `PUT` события из карточки; `PUT` календаря на `/calendars`.
- Поиск (`/search`): query/filters в URL params (восстановление при возврате); chip type фильтрует
discovery tops без ухода из Popular; в строке результата — id snippet и `calendar_title` для event.
- CRUD своих календарей — `/calendars` (из «Ещё»). Legacy `/discover`, `/calendars/:id` → redirects.
- На мобиле — bottom tab bar (+ safe-area); на desktop — sticky header + табы.
Nav `aria-current` синхронизирован с `useLocation` (без remount `Outlet` по pathname).
- Mood themes — см. §3.2.
## 3.2. Настроения (mood themes)
Пользователь переключает визуальное настроение; атрибут `html[data-mood]`. Для гостя и при отсутствии `preferences.mood` — дефолт `calm` (в профиль при логине не пишется). После входа смена mood сохраняется в `preferences.mood` через `PATCH /v1/user/me`.
| Mood | Идентификатор | Характер |
|------|---------------|----------|
| Спокойный | `calm` | sage/mist, мягкий градиент (default) |
| Энергичный | `energetic` | тёмный ink + coral/amber |
| Деловой | `business` | charcoal + steel, более жёсткие радиусы |
Переключатели mood и языка: на экранах auth (`/login`, `/register`, `/verify`) и в `/more`. В шапке workspace — только бренд, email и выход (без mood/lang).
Язык UI (`ru`/`en`): до логина — `navigator.language`; после входа — поле `language` профиля (`PATCH /v1/user/me`). Если в профиле язык пуст — при логине записывается текущий (браузерный/выбранный на экране входа). Язык также можно сменить в форме `/profile`.
## 4. Маршруты
Публичные:
- `/login`, `/register`, `/verify`
Защищённые (`ProtectedRoute`):
- `/` — Calendar Workspace (автовыбор своего календаря или empty-state)
- `/c/:calendarId` — workspace с календарём
- `/c/:calendarId/e/:eventId` — workspace + карточка события
- `/search` — поиск календарей/событий
- `/bookings` — мои бронирования (deep-link в `/c/.../e/...`)
- `/calendars` — управление своими календарями (CRUD)
- `/more` — вторичное меню + mood switcher
- `/following` — отслеживаемые чужие календари (follow; не платная Подписка)
- `/reviews`, `/subscription`, `/tickets`, `/profile`
Redirects: `/discover``/search`; `/calendars/:id``/c/:id`; `/calendars/:id/events/:eventId``/c/:id/e/:eventId`.
Состояние вида: Zustand `calendarContextStore` (`view`, `cursorDate`, last own `calendarId` в `localStorage`).
## 5. Модули
### 5.1. Auth
Регистрация, верификация, вход, восстановление сессии.
### 5.2. Профиль
`GET /v1/user/me` + форма на `/profile`. Редактируемые через `PATCH /v1/user/me`: `nickname`, `phone`, `timezone`, `avatar_url`, `language`; смена пароля — `current_password` + `password`. Read-only в UI: `id`, `email`, `role`, `status`. Mood — отдельно в `/more` (`preferences.mood`). Язык при первом визите — из браузера; при первом логине без `language` — пишется в профиль. Mood по умолчанию `calm`, при логине не сидится.
### 5.3. Поиск (участник)
`GET /v1/search` — фильтры (`type`, `q`, `tags`, даты, `lat`/`lon`/`radius`, sort, order, пагинация). Без `q`/дат/tags/geo/sort — топы календарей и событий; иначе — поиск. В списке бейдж типа — `calendar`|`event` (не `personal`/`commercial`). Даты, теги, geo и сортировка свёрнуты по умолчанию. Переход к `/c/:id` или `/c/:id/e/:eventId`.
### 5.4. Календари
- Список своих: `GET /v1/calendars`
- CRUD: `POST/PUT/DELETE /v1/calendars`, `GET /v1/calendars/:id` (поле `following`)
- Follow чужого: `POST/DELETE /v1/calendars/:id/follow`, список `GET /v1/user/following`
(UI: «Отслеживать» в about, страница `/following`; не путать с `/subscription`)
- Просмотр коммерческого чужого календаря по id (доступ по правилам бэка)
- HTML month view владельца: `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — при переключении виджета на **прошедший месяц** (owner + вид «Месяц») автоматически подменяет клиентский grid серверным HTML (live/архив, задача #15). Текущий и будущие месяцы — React-виджет по `GET …/events`.
- Создание `commercial` требует активной подписки (ответ 402 → экран подписки)
### 5.5. События
- Список: `GET /v1/calendars/:calendar_id/events`
- CRUD владельца: `POST/PUT/DELETE`
- Детали: `GET /v1/events/:id`
- Вхождения: `GET /v1/events/:id/occurrences`
- Отмена вхождения: `DELETE /v1/events/:id/occurrences/:start_time`
### 5.6. Запись (bookings)
- Участник: `POST /v1/events/:id/bookings`, `GET /v1/user/bookings`, `DELETE /v1/bookings/:id`, `GET /v1/bookings/:id`
- Владелец: `GET /v1/events/:id/bookings`, `PUT /v1/bookings/:id` с `{ action: confirm | decline }`
- Статусы: `pending` | `confirmed` | `cancelled` (режим confirmation календаря: `auto` | `manual` | timeout)
### 5.7. Отзывы
`GET/POST /v1/reviews`, `GET/PUT/DELETE /v1/reviews/:id`, `GET /v1/user/reviews`. Цели: `event` | `calendar`.
Голос: `PUT /v1/reviews/:id/vote` `{ "value": "like"|"dislike" }`, `DELETE /v1/reviews/:id/vote`; в ответах — `my_vote`.
UI: форма отзыва скрыта без confirmed booking (event — booking на это событие; calendar — booking на событие календаря); подсказка + жалоба остаются.
### 5.8. Жалобы
`POST /v1/reports``target_type`: `event` | `calendar` | `review`. Жалоба не требует booking.
### 5.9. Подписка
`GET /v1/subscription`, `POST /v1/subscription` (`start_trial` | `activate` + `plan` + опционально `payment_info`).
Планы и цены (как Back `plan_price/1`, minor units → ₽): monthly **999**, quarterly **2499**, biannual **4499**, annual **7999**.
UI: карточки сравнения, локализованный «Бесплатно» без подписки, trial, демо-оплата (шлюз-заглушка).
Создание commercial-календаря без подписки → `402` → редирект на `/subscription`.
### 5.10. Тикеты
- ErrorBoundary / необработанные ошибки → `POST /v1/tickets` с `source=frontend`
- Форма «Сообщить о баге» → `source=manual`
- Список: `GET /v1/tickets`, детали `GET /v1/tickets/:id`
### 5.11. Realtime
Подписка WS на открытые календари; сообщения `calendar_update`, `event_update`, `booking_update` → инвалидация React Query.
### 5.12. Версии сборки
Как в Admin UI / Back (`EventHubSpec/WORKFLOW.md` §6):
- Product `MAJOR.MINOR` из `EventHubSpec/VERSION` (CI: `scripts/resolve-product-version.sh`; локальный `VERSION` — fallback).
- Bake: `VITE_APP_VERSION`, `VITE_APP_BUILD`, `VITE_GIT_SHA`, `VITE_BUILT_AT`.
- UI: экран «Ещё» — `UI {version}.{build} (sha)`; API — `GET /health``API …`.
## 6. Структура проекта
```
src/
├── api/ — Axios client + *Api.ts
├── hooks/ — React Query hooks
├── store/ — Zustand (auth)
├── pages/ — auth, discover, calendars, events, bookings, reviews, subscription, tickets, profile
├── layouts/ — consumer shell (не admin Control Center)
├── components/ui — shadcn primitives
├── i18n/ — locales ru/en
├── schemas/ — Zod
└── types/
```
Репозиторий: [EventHubFront](https://git.sabilin.com/EventHub/EventHubFront).
## 6.1. Стенды и балансировщик
| Stand | URL | Swarm |
|-------|-----|--------|
| IFT | `https://ui.ift.eventhub.local` | alias `client-ui` |
| stage | `https://ui.stage.eventhub.local` | alias `client-ui` |
| dev | `https://ui.dev.eventhub.local` | alias `client-ui` |
Образ registry: `git.sabilin.com/eventhub/eventhub-front`. Traefik host-based (без path prefix). Nginx в контейнере: `/` SPA, `/v1/` → eventhub:8080, `/ws` → eventhub:8081.
CI: lint → build → Playwright **mock** (до push) → push image → deploy IFT → **e2e-ift (live API)** → stage → **e2e-stage (live API)** (зеркало FrontAdmin). Деплой зависит от EventHubDevOps#7 (`deploy-service.sh … client`).
E2E: `e2e/TESTIDS.md`. Моки (`npm run test:e2e`, `project=mock`) — только локально / до деплоя. После деплоя на IFT/stage — `SMOKE_USER_*` + `npm run test:e2e:ift|stage` против реального API (без Playwright route-моков).
## 7. Ограничения MVP (Future)
Не реализуется в UI, пока нет user HTTP API:
- приглашения / шаринг календаря (`calendar_share`)
- CRUD специалистов (`calendar_specialist`)
- серверный logout / revoke session
- загрузка файлов (вложения)
- явный `client_type=mobile` (фаза 3 бэка)
- push-уведомления (таблица `notification` без полноценной доставки)
Лайки/дизлайки отзывов: `PUT/DELETE /v1/reviews/:id/vote`, поле `my_vote` в ответах отзывов (EventHubBack#47).
## 8. Источники истины
- Маршруты и поведение: `EventHubBack/src/eventhub_app.erl` + handlers
- Продукт и auth: `EventHubBackSpec.md` §12, §6.1
- Swagger `client-swagger.json` — вторичный (возможен drift, например `/v1/auth/refresh` vs `/v1/refresh`)