composables, align with updated API
- Replace deleted Pinia stores with
module-level singleton composables
(useAuth, useChat, useFeed, useUi) — all
return reactive({...}) so
Refs auto-unwrap in both templates and
script code
- Align entire codebase with new
swagger-generated api.ts types:
· TagDto.value (was .name) — FeedCard,
FeedFilters, ProfileEditor,
ProfileSetupView, MyProfileView,
ProfileDetailView, useUi
· MediaItemDto[] / .path (was mediaUrls[],
avatarUrl) — FeedCard,
FeedView, MyProfileView,
ProfileDetailView
· ChatDto.status 'active'|'closed' (was
isActive: boolean) —
ChatRoomView, ChatsListView
· MessageDto.profileId (was senderId) —
ChatRoomView, ChatBubble
· MeResponseDto → fetchMe now calls /me +
/profiles/my in parallel
· Token refresh: res.data.data.accessToken
(nested wrapper) —
router/index.ts aligned with client.ts
interceptor
- Fix FeedCard, ChatBubble imports pointing
to deleted store files
- Fix ProfileSetupView form type to avoid
string|undefined on v-model
- Fix history.back() → window.history.back()
via goBack() helper
- Fix chat.unreadCount possibly-undefined
guard in ChatsListView
- Fix MapPicker Leaflet icon cast (as unknown
as Record<string, unknown>)
319 lines
13 KiB
Markdown
319 lines
13 KiB
Markdown
# 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 с файлом.
|