This commit is contained in:
Oscar
2026-06-08 13:23:20 +03:00
commit 637dddf656
160 changed files with 56097 additions and 0 deletions

318
README.md Normal file
View File

@@ -0,0 +1,318 @@
# Daiting — 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 | Pinia |
| 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
# Запустить в браузере (порт 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, порт 1420
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 с файлом.