✨ 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 для создания и получения встреч
342 lines
15 KiB
Markdown
342 lines
15 KiB
Markdown
# Стартовый промт — 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`).
|