Files
dating-app-frontend/README.md
2026-06-08 15:09:53 +03:00

322 lines
13 KiB
Markdown
Raw 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.
# Dating — Frontend
Vue 3 + Vite + Tauri v2. Работает как PWA в браузере и как нативное десктопное приложение (Windows / macOS / Linux).
## Стек
| Слой | Технология |
| ---------------- | ----------------------------------------------- | --- |
| UI framework | Vue 3 (Composition API, `<script setup>`) |
| Build tool | Vite 6 |
| Desktop shell | Tauri v2 |
| State management | Composables | |
| Routing | Vue Router 4 |
| HTTP client | Axios (сгенерированный клиент `src/api/api.ts`) |
| Форм-валидация | Vuelidate |
| Анимации | GSAP 3 |
| Карты | Leaflet |
| Утилиты | VueUse |
| CSS | SCSS + Tailwind v4 (reset-only) |
| Package manager | pnpm |
| TypeScript | строгий режим, `no any` |
---
## Быстрый старт
### Зависимости
- Node.js ≥ 20
- pnpm ≥ 9 (`npm i -g pnpm`)
- Rust + cargo (для Tauri: [rustup.rs](https://rustup.rs))
- Tauri CLI устанавливается через pnpm как dev-зависимость
```bash
# Клонировать репо
git clone <repo-url>
cd dating-app-frontend
# Установить зависимости
pnpm install
# Запустить в браузере (порт 3000) (old 1420)
pnpm dev
# Запустить с Tauri (нативное окно)
pnpm tauri dev
```
### Переменные окружения
Создайте `.env.local` в корне:
```env
VITE_API_BASE_URL=http://localhost:3000
```
По умолчанию фоллбэк — `http://localhost:3000`.
---
## Структура проекта
```
dating-app-frontend/
├── src/
│ ├── api/
│ │ ├── api.ts # Сгенерированный клиент (не трогать)
│ │ └── client.ts # Axios-инстанс + интерцептор refresh-токена
│ ├── assets/
│ │ └── grain.svg # SVG-текстура шума для фона
│ ├── components/
│ │ ├── common/ # AppButton, AppInput, AppModal, AppDrawer,
│ │ │ # AppToast, LoadingSpinner, EmptyState
│ │ ├── layout/ # AppShell, SideNav, BottomNav, TauriTitlebar
│ │ ├── feed/ # FeedCard, FeedCardStack, FeedFilters, ProfileBadge
│ │ ├── chat/ # ChatBubble, ChatInput, VoiceRecorder, MediaMessage
│ │ ├── profile/ # ProfileEditor, MediaGallery
│ │ ├── dates/ # MapPicker, DateProposalForm
│ │ └── reports/ # ReportModal
│ ├── composables/ # useAuth, useFeed, useChat, useMedia, useGeolocation
│ ├── router/
│ │ └── index.ts # Маршруты + guards + восстановление сессии
│ ├── stores/
│ │ ├── auth.store.ts # user, accessToken (in-memory), profiles
│ │ ├── feed.store.ts # cards, filters, pagination
│ │ ├── chat.store.ts # chats, messages, polling
│ │ ├── profile.store.ts
│ │ └── ui.store.ts # toasts, sidebar state, reference data (tags/cities)
│ ├── styles/
│ │ ├── _variables.scss # CSS custom properties + SCSS breakpoint-миксины
│ │ ├── _typography.scss
│ │ ├── _animations.scss
│ │ ├── main.scss # Глобальные стили (без Tailwind)
│ │ └── tailwind.css # @import "tailwindcss" — отдельно от SCSS
│ ├── views/ # LoginView, RegisterView, ProfileSetupView,
│ │ # FeedView, MatchesView, ChatsListView, ChatRoomView,
│ │ # DatesView, MyProfileView, ProfileDetailView,
│ │ # ReportsView (admin)
│ ├── App.vue
│ ├── main.ts
│ └── vite-env.d.ts
├── src-tauri/
│ ├── src/
│ │ ├── lib.rs # Tauri plugins init
│ │ └── main.rs # Windows entry (no console window)
│ ├── capabilities/
│ │ └── default.json # Tauri permissions
│ ├── Cargo.toml
│ ├── build.rs
│ └── tauri.conf.json
├── PRODUCT.md # Стратегический документ для AI-ассистентов
├── vite.config.ts
├── tsconfig.json
└── package.json
```
---
## Маршрутизация
| Путь | Компонент | Guard |
| --------------------- | ------------------ | ------------ |
| `/` | redirect → `/feed` | — |
| `/login` | LoginView | guest only |
| `/register` | RegisterView | guest only |
| `/setup` | ProfileSetupView | auth |
| `/feed` | FeedView | auth |
| `/matches` | MatchesView | auth |
| `/chats` | ChatsListView | auth |
| `/chats/:chatId` | ChatRoomView | auth |
| `/dates` | DatesView | auth |
| `/profile/me` | MyProfileView | auth |
| `/profile/:profileId` | ProfileDetailView | auth |
| `/admin/reports` | ReportsView | auth + admin |
Логика guard:
1. При первой навигации пытается восстановить сессию через `refreshToken` из localStorage.
2. Если пользователь авторизован, но профилей нет — редирект на `/setup`.
3. Если токен истёк — silent refresh через `POST /api/v1/auth/refresh`.
---
## Работа с токенами
```
Access token → хранится только в памяти (Pinia store / переменная модуля)
Refresh token → localStorage ('refreshToken')
```
`src/api/client.ts` содержит Axios response interceptor: при получении 401 пытается один раз обновить токен. Если не удаётся — чистит сессию и редиректит на `/login`. Параллельные запросы ставятся в очередь и резолвятся с новым токеном.
---
## Управление состоянием
### auth.store
```ts
state: { user, accessToken (in-memory ref), activeProfileId }
actions: login, logout, register, fetchMe, setActiveProfile,
addProfile, updateProfile, removeProfile
```
### feed.store
```ts
state: { cards[], filters, page, hasMore, searchPaused, loading }
actions: fetchNextPage(profileId), applyFilters(filters), removeCard(profileId)
```
### chat.store
```ts
state: { chats[], activeChat, messages[], pollingTimer }
actions: fetchChats, fetchMessages, sendMessage, openChat, closeChat,
startPolling, stopPolling
// Polling интервал: 2000 мс. Место замены на WebSocket помечено комментарием.
```
### ui.store
```ts
state: { toasts[], sidebarExpanded, tags[], cities[], districts{}, greetings[] }
actions: addToast(message, type, duration), removeToast(id), setTags/Cities/...
```
---
## SCSS-архитектура
Tailwind v4 импортируется **отдельно** от SCSS:
```ts
// main.ts — порядок важен
import '@/styles/tailwind.css' // @import "tailwindcss"
import '@/styles/main.scss' // custom styles
```
Каждый SCSS-парциал (`_typography.scss`, `_animations.scss`) включает в начале:
```scss
@use 'variables' as *;
```
Это нужно, так как Sass module system (`@use`) имеет пофайловую область видимости — миксины не наследуются транзитивно.
Компоненты Vue используют `<style scoped lang="scss">`. Vite через `additionalData` в `vite.config.ts` автоматически инжектирует `@use "@/styles/_variables.scss" as *;` во все `<style lang="scss">` блоки компонентов (кроме самих файлов стилей, чтобы избежать цикличности).
### Дизайн-токены (CSS custom properties)
```scss
--color-base:
#0d0d0d // фон приложения
--color-surface: #161614 // поверхность карточек
--color-surface-2: #1e1e1b // инпуты, вторичные поверхности
--color-cream: #f0ebe0 // основной текст
--color-muted: #6b6860 // второстепенный текст
--color-signal: #c45c3a // акцент (CTA, лайк, активный стейт)
--font-display: 'Instrument Serif',
serif --font-mono: 'DM Mono', monospace;
```
Шрифты загружаются из Google Fonts в `index.html`.
---
## Tauri
### Обнаружение среды
```ts
const isTauri = typeof window !== 'undefined' && !!window.__TAURI__
```
### Кастомный тайтлбар
`TauriTitlebar.vue` рендерится только при `isTauri === true`. Контейнер `AppShell.vue` использует `data-tauri-drag-region` для нативного drag окна.
### Файловый диалог
В `ChatInput.vue` и `MediaGallery.vue` при `isTauri` используется `@tauri-apps/plugin-dialog` (`open()`), иначе — стандартный `<input type="file">`.
### Конфигурация окна
`src-tauri/tauri.conf.json`:
- `decorations: false` — убирает системный тайтлбар
- `minWidth: 375` — соответствует мобильному брейкпойнту
- Permissions: `dialog:allow-open`, `dialog:allow-save`, `shell:allow-open`
---
## API-клиент
`src/api/api.ts` — сгенерированный файл (swagger-typescript-api). **Не редактировать вручную.**
`src/api/client.ts` — обёртка:
```ts
import { apiClient } from '@/api/client'
// Пример вызова
const feed = await apiClient.api.feedControllerGetFeed({
profileId: 'xxx',
page: 1,
limit: 20,
})
```
Все методы возвращают `Promise<void>` по типам (ограничение генератора) — кастуйте через `as unknown as YourType`.
---
## Скрипты
```bash
pnpm dev # Vite dev server, порт 3000
pnpm build # vue-tsc + vite build → dist/
pnpm preview # Превью prod-сборки
pnpm tauri dev # Tauri dev window (запускает pnpm dev внутри)
pnpm tauri build # Нативный инсталлятор → src-tauri/target/release/bundle/
```
---
## Разработка компонентов
### Новый view
1. Создать файл в `src/views/<section>/`.
2. Добавить маршрут в `src/router/index.ts` с нужными `meta` (`auth`, `admin`).
3. Лениво импортировать: `component: () => import('@/views/...')`.
### Новый компонент
- `<script setup lang="ts">` — обязательно.
- `<style scoped lang="scss">` — изолированные стили.
- Никаких `any` — типы из `src/api/api.ts` или кастомные интерфейсы в сторе/компоненте.
- Ошибки API — через `uiStore.addToast(message, 'error')`, не `console.error`.
- Скелетоны вместо спиннеров для контента с историей (лента, чат, список).
### Анимации
GSAP используется в `FeedCard.vue` для drag-механики. Все GSAP-вызовы оборачивайте в проверку:
```ts
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!prefersReducedMotion) {
gsap.to(el, { ... })
}
```
---
## Известные ограничения
- **Типы API**: генератор возвращает `Promise<void>` для всех методов. Реальные типы ответов приходится кастовать через `as unknown as T`. При регенерации клиента это сохраняется.
- **Polling**: чат поллит каждые 2 секунды. Место замены на WebSocket отмечено комментарием `// TODO: replace with WebSocket subscription` в `chat.store.ts`.
- **FCM**: `POST /api/v1/auth/fcm-token` вызывается как заглушка. Реальная интеграция требует Tauri-плагина для push-уведомлений.
- **Upload**: `mediaControllerUpload` в `MediaGallery.vue` отправляет запрос без тела — нужно доработать под реальный multipart/form-data с файлом.