feat(src/modules/cities/cities.controller.ts): добавляет ответы API для получения всех городов и районов

 feat(src/modules/chat/chat.controller.ts): добавляет ответы API для создания чата и получения сообщений

 feat(src/modules/greetings/greetings.controller.ts): добавляет ответы API для получения и создания приветствий

 feat(src/modules/likes/likes.controller.ts): добавляет ответы API для создания лайков и получения совпадений

 feat(src/modules/reports/reports.controller.ts): добавляет ответы API для создания и получения отчетов

 feat(src/modules/feed/feed.controller.ts): добавляет ответ API для получения отфильтрованного фида

 feat(src/auth/auth.controller.ts): добавляет ответы API для регистрации, входа и выхода пользователей

 feat(src/modules/media/media.controller.ts): добавляет ответы API для загрузки и получения медиа

 feat(src/modules/users/users.controller.ts): добавляет ответы API для получения текущего пользователя и управления пользователями

 feat(src/modules/tags/tags.controller.ts): добавляет ответы API для получения и создания тегов

 feat(src/modules/profiles/profiles.controller.ts): добавляет ответы API для управления профилями пользователей

 feat(src/modules/dates/dates.controller.ts): добавляет ответы API для создания и получения встреч
This commit is contained in:
Oscar
2026-06-08 14:22:50 +03:00
parent bc3e48bcad
commit 102b6b4026
24 changed files with 598 additions and 12 deletions

341
frontend-starter-prompt.md Normal file
View File

@@ -0,0 +1,341 @@
# Стартовый промт — Frontend Dating App
## Контекст проекта
Ты помогаешь разрабатывать фронтенд мобильного/десктопного дейтинг-приложения.
OpenAPI-схема бэкенда лежит в корне проекта (`openapi.json`).
Генерируй типизированный HTTP-клиент из неё автоматически.
---
## Технологический стек
| Слой | Выбор |
|---|---|
| UI-фреймворк | **Vue 3** (Composition API + `<script setup>`) |
| Язык | **TypeScript** везде |
| Сборщик | **Vite** |
| Десктоп / нативный слой | **Tauri 2** |
| Стейт-менеджер | **Pinia** |
| Роутер | **Vue Router 4** |
| HTTP-клиент | **axios** + автогенерированные типы из OpenAPI (`openapi-typescript` + `axios`) |
| WebSocket | **socket.io-client** |
| UI-библиотека | **Naive UI** (или UnoCSS + headless — на твоё усмотрение) |
| Формы | **VeeValidate** + **Zod** |
| Пакетный менеджер | **pnpm** |
| Анимации | **@vueuse/motion** |
| Утилиты | **VueUse** |
| Иконки | **@iconify/vue** |
| Адаптивность | mobile-first; десктоп — расширение, не отдельная версия |
---
## Что такое это приложение
Дейтинг-приложение. Пользователь:
1. Регистрируется по номеру телефона + пароль
2. Создаёт один или несколько публичных **профилей** (у одного аккаунта может быть много профилей)
3. Листает **ленту** других профилей с фильтрацией
4. Ставит **лайки / дизлайки** на профили
5. При взаимном лайке создаётся **матч**
6. Открывает **чат** с одним матчем за раз (один активный чат на профиль)
7. В чате обменивается текстом, фото, голосовыми и видео-сообщениями
8. Договаривается о реальных **встречах** (дата, координаты, статус)
9. Может подать **жалобу** на профиль или сообщение
---
## Архитектура бэкенда (важно для интеграции)
### Базовый URL
```
http://localhost:3000/api/v1
```
### Формат ответа
**Все** успешные ответы обёрнуты:
```json
{ "data": <payload> }
```
Ошибки:
```json
{ "statusCode": 400, "message": "...", "timestamp": "...", "path": "..." }
```
### Авторизация
- Тип: **Bearer JWT**
- Заголовок: `Authorization: Bearer <accessToken>`
- `POST /auth/register` — регистрация `{ phone, password }`
- `POST /auth/login` — вход `{ phone, password }``{ accessToken, refreshToken }`
- `POST /auth/refresh` — обновление `{ refreshToken }``{ accessToken, refreshToken }`
- `POST /auth/logout` — выход (требует Bearer)
- `POST /auth/fcm-token` — обновить push-токен `{ fcmToken }`
- Access-токен: TTL **7 дней** (настраивается на бэке)
- Refresh-токен: TTL **30 дней**, хранится в Redis
### Профили (один аккаунт → много профилей)
- `POST /profiles` — создать профиль
- `GET /profiles/my` — все мои профили
- `GET /profiles/:profileId` — публичный профиль
- `PUT /profiles/:profileId` — обновить (только владелец)
- `DELETE /profiles/:profileId` — удалить (только владелец)
Поля профиля: `name`, `birthDate`, `gender` (`male|female`), `cityId`, `districtId`,
`description`, `nation`, `height`, `weight`, `tagIds[]`
Ответ профиля включает: `city`, `district`, `tags[]`, `media[]` (отсортировано по `sortOrder`)
### Медиа профиля
Маршруты: `POST /profiles/:profileId/media/upload?type=photo|video|audio`
Медиафайлы профиля: **фото, видео, аудио** — хранятся в MinIO.
В ответе профиля приходит массив `media[]` с полями `{ id, path, type, sortOrder }`.
### Лента
```
GET /feed?profileId=<uuid>&page=1&limit=20
&cityId=&districtId=
&ageMin=&ageMax=
&keyword=
&tagIds[]=
```
- Уже лайкнутые профили **не показываются**
- Порядок случайный
- При достижении лимита матчей (`MAX_MATCHES_BEFORE_PAUSE=10`) лайк вернёт `400`
клиент должен показать экран «разбери матчи»
### Лайки и матчи
- `POST /likes``{ sourceProfileId, targetProfileId, type: "like"|"dislike" }`
Ответ: `{ like, match }` — если `match != null`, это взаимный лайк
- `GET /likes/matches?profileId=<uuid>` — все матчи профиля
### Чат
- Один профиль — один активный чат одновременно (`profile.active_chat_id`)
- `POST /chats``{ profileId, matchId }` → открыть чат для матча
- `GET /chats?profileId=<uuid>` — активные чаты
- `GET /chats/:chatId/messages?profileId=<uuid>&page=1&limit=50`
- `POST /chats/:chatId/messages?profileId=<uuid>` — отправить `{ text?, mediaUrl?, mediaType? }`
- `DELETE /chats/:chatId?profileId=<uuid>` — закрыть чат
### Встречи (dates)
- `POST /dates``{ profileId, partnerProfileId, lat, lng, time, statusId? }`
- `GET /dates?profileId=<uuid>`
- `PATCH /dates/:id/status?profileId=<uuid>``{ statusId }`
- `GET /dates/statuses` — справочник статусов (`pending`, `confirmed`, `cancelled`, `rescheduled`)
### Репорты
- `POST /reports``{ sourceProfileId, entityId, entityType: "profile"|"message", description? }`
### Справочники (публичные, без токена)
- `GET /tags`
- `GET /cities`
- `GET /cities/:cityId/districts`
- `GET /greetings` — готовые приветственные фразы
---
## WebSocket (real-time чат)
Библиотека: **socket.io-client**
```typescript
const socket = io('http://localhost:3000/chat', {
auth: {
token: '<accessToken>',
profileId: '<activeProfileId>', // обязательно!
}
})
```
**Клиент отправляет:**
| Событие | Payload | Описание |
|---|---|---|
| `join_chat` | `{ chatId }` | Войти в комнату чата |
| `leave_chat` | `{ chatId }` | Покинуть комнату |
| `send_message` | `{ chatId, text?, mediaUrl?, mediaType? }` | Отправить сообщение |
| `typing` | `{ chatId, isTyping: bool }` | Индикатор печати |
**Сервер отправляет:**
| Событие | Payload | Описание |
|---|---|---|
| `new_message` | `Message` | Новое сообщение в комнате |
| `user_typing` | `{ profileId, isTyping }` | Кто-то печатает |
**Важно:** при смене активного профиля переподключи сокет с новым `profileId`.
---
## Структура проекта
```
src/
api/ # Автогенерированный клиент из openapi.json
client.ts # axios-инстанс с интерцептором токена и рефреша
index.ts # реэкспорт всех операций
stores/
auth.store.ts # user, accessToken, refreshToken, login/logout
profile.store.ts # activeProfile, myProfiles, switch
feed.store.ts # лента, пагинация
chat.store.ts # активный чат, сообщения, WS-соединение
match.store.ts # матчи
composables/
useSocket.ts # хук socket.io с авто-реконнектом
useInfiniteScroll.ts # бесконечная прокрутка
useMediaUpload.ts # загрузка файлов с прогрессом
router/
index.ts
guards.ts # редирект на /login если нет токена, /setup-profile если нет профиля
views/
auth/
LoginView.vue
RegisterView.vue
onboarding/
CreateProfileView.vue # создание первого профиля после регистрации
feed/
FeedView.vue # свайп-лента или карточки
matches/
MatchesView.vue
chat/
ChatsListView.vue
ChatView.vue # реалтайм переписка
dates/
DatesView.vue
profile/
MyProfilesView.vue # список профилей аккаунта
ProfileEditView.vue
ProfilePublicView.vue # как видят другие
settings/
SettingsView.vue
components/
profile/
ProfileCard.vue # карточка в ленте
ProfileAvatar.vue
MediaGallery.vue # фото/видео/аудио галерея профиля
MediaUploader.vue # загрузка с preview
chat/
MessageBubble.vue
TypingIndicator.vue
AudioPlayer.vue # проигрыватель голосовых
feed/
SwipeCard.vue # свайп-карточка (мобильная)
LikeButtons.vue
common/
BottomNav.vue # навигация для мобилки
SideNav.vue # навигация для десктопа
AppLayout.vue # адаптивный layout
tauri/ # Tauri-специфичный код
notifications.ts # нативные уведомления
deepLinks.ts
```
---
## Адаптивность: мобилка и десктоп в одном приложении
Приложение собирается через **Tauri 2**. Один кодовой базой покрывается:
- **Мобилка** (Tauri + iOS/Android через Tauri Mobile) — свайп-жесты, нижняя навигация, полноэкранные карточки
- **Десктоп** (Tauri + Windows/macOS/Linux) — боковое меню, двухколоночный layout, drag-and-drop для медиа
Используй CSS-брейкпоинты **mobile-first**:
- `< 768px` — мобильный layout (BottomNav, полноэкранная лента)
- `≥ 768px` — планшет/десктоп (SideNav, split-view для чата)
Определяй режим через `window.innerWidth` или VueUse `useBreakpoints`.
Tauri-специфичное поведение (нативные уведомления, файловый пикер) оборачивай в composable с graceful degradation до браузерного API.
---
## Ключевые UX-сценарии
### 1. Первый запуск
`Регистрация``Создание профиля` (name, birthDate, gender обязательны) →
`Загрузка фото``Лента`
### 2. Лента и лайк
Карточка профиля → свайп вправо (лайк) / влево (дизлайк) → если матч →
показать `MatchModal` с кнопкой «Написать» → открыть чат
### 3. Лимит матчей
Попытка лайкнуть при `N >= 10 матчей` → API вернёт `400`
показать экран/модал «У тебя X матчей. Разберись с ними, чтобы продолжить поиск»
### 4. Чат
Список чатов → выбор → join WS-комнаты → обмен сообщениями в реалтайме →
закрытие чата = новый чат стал доступен.
Перед открытием чата — можно выбрать приветственную фразу из `GET /greetings`.
### 5. Смена активного профиля
В настройках — список всех профилей аккаунта →
при смене переподключить WS с новым `profileId`.
### 6. Встреча
В активном чате кнопка «Назначить встречу» → форма (карта/координаты + дата) →
`POST /dates` → партнёр видит `pending` → может подтвердить / отменить / перенести.
---
## Хранение состояния авторизации
```typescript
// stores/auth.store.ts (Pinia)
// accessToken + refreshToken хранить в localStorage
// При 401 — автоматически вызывать POST /auth/refresh
// При неудаче refresh — очистить стор, редирект на /login
```
Интерцептор axios:
```typescript
// При 401: один раз попытаться рефрешнуть токен,
// повторить исходный запрос с новым токеном,
// если рефреш тоже 401 — logout
```
---
## Нотификации
- В браузере / PWA: Web Notifications API
- В Tauri: `@tauri-apps/plugin-notification`
- FCM-токен: после логина отправить на `POST /auth/fcm-token`
- Обрабатывать WS-события `new_message` и `match_created` для in-app уведомлений
---
## Генерация API-клиента
В корне проекта лежит `openapi.json`. Сгенерируй типизированный клиент:
```bash
pnpm add -D openapi-typescript
pnpm exec openapi-typescript openapi.json -o src/api/schema.d.ts
```
Используй типы из схемы для всех запросов/ответов.
Создай обёртку над axios, которая автоматически разворачивает `{ data: T }`.
---
## Что НЕ нужно реализовывать сейчас
- Логика оплаты (таблицы `tariff`/`payment` есть в БД, но API оплаты нет)
- Админ-панель (модераторские эндпоинты — ban/activate пользователей — есть в API, но отдельного UI не нужно)
---
## Старт проекта
```bash
pnpm create tauri-app@latest
# выбрать: Vue + TypeScript + Vite
pnpm add pinia vue-router axios socket.io-client vee-validate zod @vueuse/core @vueuse/motion naive-ui @iconify/vue
pnpm add -D openapi-typescript @types/node
# Сгенерировать типы из схемы
pnpm exec openapi-typescript openapi.json -o src/api/schema.d.ts
```
Не спрашивай подтверждения на каждый шаг — работай до тех пор,
пока весь фронтенд не будет готов к первому запуску (`pnpm tauri dev`).