refactor(composables): migrate stores →

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>)
This commit is contained in:
Oscar
2026-06-08 15:01:54 +03:00
parent f5e34f3a97
commit 10d696f4ca
41 changed files with 913 additions and 673 deletions

View File

@@ -4,21 +4,21 @@ Vue 3 + Vite + Tauri v2. Работает как PWA в браузере и ка
## Стек ## Стек
| Слой | Технология | | Слой | Технология |
|---|---| |---|-------------------------------------------------|
| UI framework | Vue 3 (Composition API, `<script setup>`) | | UI framework | Vue 3 (Composition API, `<script setup>`) |
| Build tool | Vite 6 | | Build tool | Vite 6 |
| Desktop shell | Tauri v2 | | Desktop shell | Tauri v2 |
| State management | Pinia | | State management | Composables | |
| Routing | Vue Router 4 | | Routing | Vue Router 4 |
| HTTP client | Axios (сгенерированный клиент `src/api/api.ts`) | | HTTP client | Axios (сгенерированный клиент `src/api/api.ts`) |
| Форм-валидация | Vuelidate | | Форм-валидация | Vuelidate |
| Анимации | GSAP 3 | | Анимации | GSAP 3 |
| Карты | Leaflet | | Карты | Leaflet |
| Утилиты | VueUse | | Утилиты | VueUse |
| CSS | SCSS + Tailwind v4 (reset-only) | | CSS | SCSS + Tailwind v4 (reset-only) |
| Package manager | pnpm | | Package manager | pnpm |
| TypeScript | строгий режим, `no any` | | TypeScript | строгий режим, `no any` |
--- ---

View File

@@ -8,7 +8,7 @@
"build": "vue-tsc && vite build", "build": "vue-tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri", "tauri": "tauri",
"get:api": "npx swagger-typescript-api@12.0.4 -p http://localhost:1337/api/docs-json -o ./src/api -n api.ts --axios --unwrap-response-data --extract-request-params --extract-request-body --single-http-client" "gen:api": "npx swagger-typescript-api@12.0.4 -p http://localhost:1337/api/docs-json -o ./src/api -n api.ts --axios --unwrap-response-data --extract-request-params --extract-request-body --single-http-client"
}, },
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.1.5", "@floating-ui/vue": "^1.1.5",
@@ -22,7 +22,6 @@
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"gsap": "^3.12.5", "gsap": "^3.12.5",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"pinia": "^2.2.2",
"vue": "^3.5.6", "vue": "^3.5.6",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },

22
pnpm-lock.yaml generated
View File

@@ -43,9 +43,6 @@ importers:
leaflet: leaflet:
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4 version: 1.9.4
pinia:
specifier: ^2.2.2
version: 2.3.1(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))
vue: vue:
specifier: ^3.5.6 specifier: ^3.5.6
version: 3.5.35(typescript@5.9.3) version: 3.5.35(typescript@5.9.3)
@@ -1278,15 +1275,6 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'} engines: {node: '>=12'}
pinia@2.3.1:
resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
postcss-value-parser@4.2.0: postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -2347,16 +2335,6 @@ snapshots:
picomatch@4.0.4: {} picomatch@4.0.4: {}
pinia@2.3.1(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.35(typescript@5.9.3)
vue-demi: 0.14.10(vue@3.5.35(typescript@5.9.3))
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- '@vue/composition-api'
postcss-value-parser@4.2.0: {} postcss-value-parser@4.2.0: {}
postcss@8.5.15: postcss@8.5.15:

View File

@@ -2,11 +2,11 @@
import { onMounted, onUnmounted } from 'vue'; import { onMounted, onUnmounted } from 'vue';
import AppShell from '@/components/layout/AppShell.vue'; import AppShell from '@/components/layout/AppShell.vue';
import AppToast from '@/components/common/AppToast.vue'; import AppToast from '@/components/common/AppToast.vue';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import type { Tag, City, Greeting } from '@/stores/ui.store'; import type { Tag, City, Greeting } from '@/composables/useUi';
const uiStore = useUiStore(); const uiStore = useUi();
async function loadReferences() { async function loadReferences() {
if (uiStore.referencesLoaded) return; if (uiStore.referencesLoaded) return;

View File

@@ -16,6 +16,13 @@ export interface RegisterDto {
password: string; password: string;
} }
export interface TokensResponseDto {
/** @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." */
accessToken: string;
/** @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." */
refreshToken: string;
}
export interface LoginDto { export interface LoginDto {
/** @example "+79991234567" */ /** @example "+79991234567" */
phone: string; phone: string;
@@ -23,10 +30,48 @@ export interface LoginDto {
password: string; password: string;
} }
export interface MessageResponseDto {
/** @example "Operation successful" */
message: string;
}
export interface RefreshTokenDto { export interface RefreshTokenDto {
refreshToken: string; refreshToken: string;
} }
export interface RoleDto {
id: string;
name: string;
}
export interface ProfileSummaryDto {
id: string;
name: string;
gender: "male" | "female";
}
export interface MeResponseDto {
id: string;
phone: string;
status: "active" | "banned" | "pending";
roleId?: string | null;
tariffId?: string | null;
paymentId?: string | null;
fcmToken?: string | null;
role?: RoleDto | null;
profiles: ProfileSummaryDto[];
}
export interface UserResponseDto {
id: string;
phone: string;
status: "active" | "banned" | "pending";
roleId?: string | null;
tariffId?: string | null;
paymentId?: string | null;
fcmToken?: string | null;
}
export interface CreateProfileDto { export interface CreateProfileDto {
name: string; name: string;
/** @example "1995-06-15" */ /** @example "1995-06-15" */
@@ -41,6 +86,52 @@ export interface CreateProfileDto {
tagIds?: string[]; tagIds?: string[];
} }
export interface CityDto {
id: string;
name: string;
lat: string;
lng: string;
}
export interface DistrictDto {
id: string;
cityId: string;
name: string;
}
export interface TagDto {
id: string;
value: string;
}
export interface MediaItemDto {
id: string;
profileId: string;
path: string;
type: "photo" | "video" | "audio";
sortOrder: number;
}
export interface ProfileResponseDto {
id: string;
userId: string;
name: string;
/** @example "1995-06-15" */
birthDate: string;
gender: "male" | "female";
cityId?: string | null;
districtId?: string | null;
description?: string | null;
nation?: string | null;
height?: number | null;
weight?: number | null;
activeChatId?: string | null;
city?: CityDto | null;
district?: DistrictDto | null;
tags: TagDto[];
media: MediaItemDto[];
}
export interface UpdateProfileDto { export interface UpdateProfileDto {
name?: string; name?: string;
/** @example "1995-06-15" */ /** @example "1995-06-15" */
@@ -63,6 +154,26 @@ export interface CreateLikeDto {
type: "like" | "dislike"; type: "like" | "dislike";
} }
export interface LikeDto {
id: string;
sourceProfileId: string;
targetProfileId: string;
type: "like" | "dislike";
createdAt: string;
}
export interface MatchDto {
id: string;
profile1Id: string;
profile2Id: string;
createdAt: string;
}
export interface CreateLikeResponseDto {
like: LikeDto;
match?: MatchDto | null;
}
export interface CreateChatDto { export interface CreateChatDto {
/** Your profile ID */ /** Your profile ID */
profileId: string; profileId: string;
@@ -70,6 +181,23 @@ export interface CreateChatDto {
matchId: string; matchId: string;
} }
export interface ChatDto {
id: string;
profile1Id: string;
profile2Id: string;
status: "active" | "closed";
}
export interface MessageDto {
id: string;
chatId: string;
profileId: string;
text?: string | null;
mediaUrl?: string | null;
mediaType?: "photo" | "voice" | "video" | null;
createdAt: string;
}
export interface SendMessageDto { export interface SendMessageDto {
text?: string; text?: string;
mediaUrl?: string; mediaUrl?: string;
@@ -87,6 +215,26 @@ export interface CreateDateDto {
statusId?: string; statusId?: string;
} }
export interface DateDto {
id: string;
profile1Id: string;
profile2Id: string;
lat: string;
lng: string;
time: string;
statusId?: string | null;
}
export interface DateStatusDto {
id: string;
text: string;
}
export interface DateWithStatusDto {
date: DateDto;
date_status?: DateStatusDto | null;
}
export interface UpdateDateStatusDto { export interface UpdateDateStatusDto {
statusId: string; statusId: string;
} }
@@ -99,6 +247,56 @@ export interface CreateReportDto {
description?: string; description?: string;
} }
export interface ReportDto {
id: string;
sourceProfileId: string;
entityId: string;
entityType: "profile" | "message";
description?: string | null;
}
export interface TagResponseDto {
id: string;
value: string;
}
export interface CityResponseDto {
id: string;
name: string;
lat: string;
lng: string;
}
export interface DistrictResponseDto {
id: string;
cityId: string;
name: string;
}
export interface CreateCityDto {
/** @example "Москва" */
name: string;
/** @example 55.7558 */
lat: number;
/** @example 37.6173 */
lng: number;
}
export interface CreateDistrictDto {
/** @example "Центральный" */
name: string;
}
export interface GreetingDto {
id: string;
text: string;
}
export interface AuthControllerUpdateFcmTokenPayload {
/** @example "firebase-token-abc123" */
fcmToken: string;
}
export interface MediaControllerUploadParams { export interface MediaControllerUploadParams {
type: string; type: string;
profileId: string; profileId: string;
@@ -129,6 +327,10 @@ export interface ChatControllerGetChatsParams {
export interface ChatControllerGetMessagesParams { export interface ChatControllerGetMessagesParams {
profileId: string; profileId: string;
/** @default 50 */
limit?: number;
/** @default 1 */
page?: number;
chatId: string; chatId: string;
} }
@@ -151,6 +353,16 @@ export interface DatesControllerUpdateStatusParams {
id: string; id: string;
} }
export interface TagsControllerCreatePayload {
/** @example "Спорт" */
value: string;
}
export interface GreetingsControllerCreatePayload {
/** @example "Привет!" */
text: string;
}
import axios, { AxiosInstance, AxiosRequestConfig, HeadersDefaults, ResponseType } from "axios"; import axios, { AxiosInstance, AxiosRequestConfig, HeadersDefaults, ResponseType } from "axios";
export type QueryParamsType = Record<string | number, any>; export type QueryParamsType = Record<string | number, any>;
@@ -307,11 +519,12 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/auth/register * @request POST:/api/v1/auth/register
*/ */
authControllerRegister: (data: RegisterDto, params: RequestParams = {}) => authControllerRegister: (data: RegisterDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<TokensResponseDto, any>({
path: `/api/v1/auth/register`, path: `/api/v1/auth/register`,
method: "POST", method: "POST",
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -324,11 +537,12 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/auth/login * @request POST:/api/v1/auth/login
*/ */
authControllerLogin: (data: LoginDto, params: RequestParams = {}) => authControllerLogin: (data: LoginDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<TokensResponseDto, any>({
path: `/api/v1/auth/login`, path: `/api/v1/auth/login`,
method: "POST", method: "POST",
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -342,10 +556,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
authControllerLogout: (params: RequestParams = {}) => authControllerLogout: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/auth/logout`, path: `/api/v1/auth/logout`,
method: "POST", method: "POST",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -358,11 +573,12 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/auth/refresh * @request POST:/api/v1/auth/refresh
*/ */
authControllerRefresh: (data: RefreshTokenDto, params: RequestParams = {}) => authControllerRefresh: (data: RefreshTokenDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<TokensResponseDto, any>({
path: `/api/v1/auth/refresh`, path: `/api/v1/auth/refresh`,
method: "POST", method: "POST",
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -375,11 +591,14 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/auth/fcm-token * @request POST:/api/v1/auth/fcm-token
* @secure * @secure
*/ */
authControllerUpdateFcmToken: (params: RequestParams = {}) => authControllerUpdateFcmToken: (data: AuthControllerUpdateFcmTokenPayload, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/auth/fcm-token`, path: `/api/v1/auth/fcm-token`,
method: "POST", method: "POST",
body: data,
secure: true, secure: true,
type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -393,10 +612,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
usersControllerGetMe: (params: RequestParams = {}) => usersControllerGetMe: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MeResponseDto, any>({
path: `/api/v1/users/me`, path: `/api/v1/users/me`,
method: "GET", method: "GET",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -410,10 +630,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
usersControllerFindOne: (id: string, params: RequestParams = {}) => usersControllerFindOne: (id: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<UserResponseDto, any>({
path: `/api/v1/users/${id}`, path: `/api/v1/users/${id}`,
method: "GET", method: "GET",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -427,10 +648,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
usersControllerBan: (id: string, params: RequestParams = {}) => usersControllerBan: (id: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/users/${id}/ban`, path: `/api/v1/users/${id}/ban`,
method: "PATCH", method: "PATCH",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -444,10 +666,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
usersControllerActivate: (id: string, params: RequestParams = {}) => usersControllerActivate: (id: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/users/${id}/activate`, path: `/api/v1/users/${id}/activate`,
method: "PATCH", method: "PATCH",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -461,12 +684,13 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
profilesControllerCreate: (data: CreateProfileDto, params: RequestParams = {}) => profilesControllerCreate: (data: CreateProfileDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ProfileResponseDto, any>({
path: `/api/v1/profiles`, path: `/api/v1/profiles`,
method: "POST", method: "POST",
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -480,10 +704,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
profilesControllerGetMyProfiles: (params: RequestParams = {}) => profilesControllerGetMyProfiles: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ProfileResponseDto[], any>({
path: `/api/v1/profiles/my`, path: `/api/v1/profiles/my`,
method: "GET", method: "GET",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -497,12 +722,13 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
profilesControllerUpdate: (profileId: string, data: UpdateProfileDto, params: RequestParams = {}) => profilesControllerUpdate: (profileId: string, data: UpdateProfileDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ProfileResponseDto, any>({
path: `/api/v1/profiles/${profileId}`, path: `/api/v1/profiles/${profileId}`,
method: "PUT", method: "PUT",
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -516,10 +742,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
profilesControllerFindOne: (profileId: string, params: RequestParams = {}) => profilesControllerFindOne: (profileId: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ProfileResponseDto, any>({
path: `/api/v1/profiles/${profileId}`, path: `/api/v1/profiles/${profileId}`,
method: "GET", method: "GET",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -533,10 +760,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
profilesControllerDelete: (profileId: string, params: RequestParams = {}) => profilesControllerDelete: (profileId: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/profiles/${profileId}`, path: `/api/v1/profiles/${profileId}`,
method: "DELETE", method: "DELETE",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -550,11 +778,12 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
mediaControllerUpload: ({ profileId, ...query }: MediaControllerUploadParams, params: RequestParams = {}) => mediaControllerUpload: ({ profileId, ...query }: MediaControllerUploadParams, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MediaItemDto, any>({
path: `/api/v1/profiles/${profileId}/media/upload`, path: `/api/v1/profiles/${profileId}/media/upload`,
method: "POST", method: "POST",
query: query, query: query,
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -568,10 +797,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
mediaControllerGetMedia: (profileId: string, params: RequestParams = {}) => mediaControllerGetMedia: (profileId: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MediaItemDto[], any>({
path: `/api/v1/profiles/${profileId}/media`, path: `/api/v1/profiles/${profileId}/media`,
method: "GET", method: "GET",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -585,10 +815,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
mediaControllerDeleteMedia: (mediaId: string, profileId: string, params: RequestParams = {}) => mediaControllerDeleteMedia: (mediaId: string, profileId: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/profiles/${profileId}/media/${mediaId}`, path: `/api/v1/profiles/${profileId}/media/${mediaId}`,
method: "DELETE", method: "DELETE",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -602,11 +833,12 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
feedControllerGetFeed: (query: FeedControllerGetFeedParams, params: RequestParams = {}) => feedControllerGetFeed: (query: FeedControllerGetFeedParams, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ProfileResponseDto[], any>({
path: `/api/v1/feed`, path: `/api/v1/feed`,
method: "GET", method: "GET",
query: query, query: query,
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -620,12 +852,13 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
likesControllerCreateLike: (data: CreateLikeDto, params: RequestParams = {}) => likesControllerCreateLike: (data: CreateLikeDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<CreateLikeResponseDto, any>({
path: `/api/v1/likes`, path: `/api/v1/likes`,
method: "POST", method: "POST",
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -639,11 +872,12 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
likesControllerGetMyMatches: (query: LikesControllerGetMyMatchesParams, params: RequestParams = {}) => likesControllerGetMyMatches: (query: LikesControllerGetMyMatchesParams, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MatchDto[], any>({
path: `/api/v1/likes/matches`, path: `/api/v1/likes/matches`,
method: "GET", method: "GET",
query: query, query: query,
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -657,12 +891,13 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
chatControllerCreateChat: (data: CreateChatDto, params: RequestParams = {}) => chatControllerCreateChat: (data: CreateChatDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ChatDto, any>({
path: `/api/v1/chats`, path: `/api/v1/chats`,
method: "POST", method: "POST",
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -676,11 +911,12 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
chatControllerGetChats: (query: ChatControllerGetChatsParams, params: RequestParams = {}) => chatControllerGetChats: (query: ChatControllerGetChatsParams, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ChatDto[], any>({
path: `/api/v1/chats`, path: `/api/v1/chats`,
method: "GET", method: "GET",
query: query, query: query,
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -694,11 +930,12 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
chatControllerGetMessages: ({ chatId, ...query }: ChatControllerGetMessagesParams, params: RequestParams = {}) => chatControllerGetMessages: ({ chatId, ...query }: ChatControllerGetMessagesParams, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageDto[], any>({
path: `/api/v1/chats/${chatId}/messages`, path: `/api/v1/chats/${chatId}/messages`,
method: "GET", method: "GET",
query: query, query: query,
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -716,13 +953,14 @@ export class Api<SecurityDataType extends unknown> {
data: SendMessageDto, data: SendMessageDto,
params: RequestParams = {}, params: RequestParams = {},
) => ) =>
this.http.request<void, any>({ this.http.request<MessageDto, any>({
path: `/api/v1/chats/${chatId}/messages`, path: `/api/v1/chats/${chatId}/messages`,
method: "POST", method: "POST",
query: query, query: query,
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -736,11 +974,12 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
chatControllerCloseChat: ({ chatId, ...query }: ChatControllerCloseChatParams, params: RequestParams = {}) => chatControllerCloseChat: ({ chatId, ...query }: ChatControllerCloseChatParams, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/chats/${chatId}`, path: `/api/v1/chats/${chatId}`,
method: "DELETE", method: "DELETE",
query: query, query: query,
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -754,12 +993,13 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
datesControllerCreate: (data: CreateDateDto, params: RequestParams = {}) => datesControllerCreate: (data: CreateDateDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<DateDto, any>({
path: `/api/v1/dates`, path: `/api/v1/dates`,
method: "POST", method: "POST",
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -773,11 +1013,12 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
datesControllerGetDates: (query: DatesControllerGetDatesParams, params: RequestParams = {}) => datesControllerGetDates: (query: DatesControllerGetDatesParams, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<DateWithStatusDto[], any>({
path: `/api/v1/dates`, path: `/api/v1/dates`,
method: "GET", method: "GET",
query: query, query: query,
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -795,13 +1036,14 @@ export class Api<SecurityDataType extends unknown> {
data: UpdateDateStatusDto, data: UpdateDateStatusDto,
params: RequestParams = {}, params: RequestParams = {},
) => ) =>
this.http.request<void, any>({ this.http.request<DateDto, any>({
path: `/api/v1/dates/${id}/status`, path: `/api/v1/dates/${id}/status`,
method: "PATCH", method: "PATCH",
query: query, query: query,
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -815,10 +1057,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
datesControllerGetStatuses: (params: RequestParams = {}) => datesControllerGetStatuses: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<DateStatusDto[], any>({
path: `/api/v1/dates/statuses`, path: `/api/v1/dates/statuses`,
method: "GET", method: "GET",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -832,12 +1075,13 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
reportsControllerCreate: (data: CreateReportDto, params: RequestParams = {}) => reportsControllerCreate: (data: CreateReportDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ReportDto, any>({
path: `/api/v1/reports`, path: `/api/v1/reports`,
method: "POST", method: "POST",
body: data, body: data,
secure: true, secure: true,
type: ContentType.Json, type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -851,10 +1095,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
reportsControllerGetAll: (params: RequestParams = {}) => reportsControllerGetAll: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<ReportDto[], any>({
path: `/api/v1/reports`, path: `/api/v1/reports`,
method: "GET", method: "GET",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -867,9 +1112,10 @@ export class Api<SecurityDataType extends unknown> {
* @request GET:/api/v1/tags * @request GET:/api/v1/tags
*/ */
tagsControllerFindAll: (params: RequestParams = {}) => tagsControllerFindAll: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<TagResponseDto[], any>({
path: `/api/v1/tags`, path: `/api/v1/tags`,
method: "GET", method: "GET",
format: "json",
...params, ...params,
}), }),
@@ -882,11 +1128,14 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/tags * @request POST:/api/v1/tags
* @secure * @secure
*/ */
tagsControllerCreate: (params: RequestParams = {}) => tagsControllerCreate: (data: TagsControllerCreatePayload, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<TagResponseDto, any>({
path: `/api/v1/tags`, path: `/api/v1/tags`,
method: "POST", method: "POST",
body: data,
secure: true, secure: true,
type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -900,10 +1149,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
tagsControllerDelete: (id: string, params: RequestParams = {}) => tagsControllerDelete: (id: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/tags/${id}`, path: `/api/v1/tags/${id}`,
method: "DELETE", method: "DELETE",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
@@ -916,9 +1166,10 @@ export class Api<SecurityDataType extends unknown> {
* @request GET:/api/v1/cities * @request GET:/api/v1/cities
*/ */
citiesControllerFindAll: (params: RequestParams = {}) => citiesControllerFindAll: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<CityResponseDto[], any>({
path: `/api/v1/cities`, path: `/api/v1/cities`,
method: "GET", method: "GET",
format: "json",
...params, ...params,
}), }),
@@ -931,11 +1182,14 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/cities * @request POST:/api/v1/cities
* @secure * @secure
*/ */
citiesControllerCreateCity: (params: RequestParams = {}) => citiesControllerCreateCity: (data: CreateCityDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<CityResponseDto, any>({
path: `/api/v1/cities`, path: `/api/v1/cities`,
method: "POST", method: "POST",
body: data,
secure: true, secure: true,
type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -948,9 +1202,10 @@ export class Api<SecurityDataType extends unknown> {
* @request GET:/api/v1/cities/{cityId}/districts * @request GET:/api/v1/cities/{cityId}/districts
*/ */
citiesControllerFindDistricts: (cityId: string, params: RequestParams = {}) => citiesControllerFindDistricts: (cityId: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<DistrictResponseDto[], any>({
path: `/api/v1/cities/${cityId}/districts`, path: `/api/v1/cities/${cityId}/districts`,
method: "GET", method: "GET",
format: "json",
...params, ...params,
}), }),
@@ -963,11 +1218,14 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/cities/{cityId}/districts * @request POST:/api/v1/cities/{cityId}/districts
* @secure * @secure
*/ */
citiesControllerCreateDistrict: (cityId: string, params: RequestParams = {}) => citiesControllerCreateDistrict: (cityId: string, data: CreateDistrictDto, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<DistrictResponseDto, any>({
path: `/api/v1/cities/${cityId}/districts`, path: `/api/v1/cities/${cityId}/districts`,
method: "POST", method: "POST",
body: data,
secure: true, secure: true,
type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -980,9 +1238,10 @@ export class Api<SecurityDataType extends unknown> {
* @request GET:/api/v1/greetings * @request GET:/api/v1/greetings
*/ */
greetingsControllerFindAll: (params: RequestParams = {}) => greetingsControllerFindAll: (params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<GreetingDto[], any>({
path: `/api/v1/greetings`, path: `/api/v1/greetings`,
method: "GET", method: "GET",
format: "json",
...params, ...params,
}), }),
@@ -995,11 +1254,14 @@ export class Api<SecurityDataType extends unknown> {
* @request POST:/api/v1/greetings * @request POST:/api/v1/greetings
* @secure * @secure
*/ */
greetingsControllerCreate: (params: RequestParams = {}) => greetingsControllerCreate: (data: GreetingsControllerCreatePayload, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<GreetingDto, any>({
path: `/api/v1/greetings`, path: `/api/v1/greetings`,
method: "POST", method: "POST",
body: data,
secure: true, secure: true,
type: ContentType.Json,
format: "json",
...params, ...params,
}), }),
@@ -1013,10 +1275,11 @@ export class Api<SecurityDataType extends unknown> {
* @secure * @secure
*/ */
greetingsControllerDelete: (id: string, params: RequestParams = {}) => greetingsControllerDelete: (id: string, params: RequestParams = {}) =>
this.http.request<void, any>({ this.http.request<MessageResponseDto, any>({
path: `/api/v1/greetings/${id}`, path: `/api/v1/greetings/${id}`,
method: "DELETE", method: "DELETE",
secure: true, secure: true,
format: "json",
...params, ...params,
}), }),
}; };

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import type { ChatMessage } from '@/stores/chat.store'; import type { ChatMessage } from '@/composables/useChat';
import MediaMessage from './MediaMessage.vue'; import MediaMessage from './MediaMessage.vue';
const props = defineProps<{ const props = defineProps<{

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
const uiStore = useUiStore(); const uiStore = useUi();
</script> </script>
<template> <template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import MapPicker from './MapPicker.vue'; import MapPicker from './MapPicker.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
@@ -10,8 +10,8 @@ import AppInput from '@/components/common/AppInput.vue';
const props = defineProps<{ partnerProfileId: string }>(); const props = defineProps<{ partnerProfileId: string }>();
const emit = defineEmits<{ close: []; created: [] }>(); const emit = defineEmits<{ close: []; created: [] }>();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const form = reactive({ const form = reactive({
time: '', time: '',

View File

@@ -16,7 +16,7 @@ onMounted(async () => {
await import('leaflet/dist/leaflet.css'); await import('leaflet/dist/leaflet.css');
// Fix Leaflet default icon paths // Fix Leaflet default icon paths
delete (L.Icon.Default.prototype as Record<string, unknown>)._getIconUrl; delete (L.Icon.Default.prototype as unknown as Record<string, unknown>)._getIconUrl;
L.Icon.Default.mergeOptions({ L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',

View File

@@ -2,7 +2,7 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'; import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
import type { FeedProfile } from '@/stores/feed.store'; import type { FeedProfile } from '@/composables/useFeed';
import ProfileBadge from './ProfileBadge.vue'; import ProfileBadge from './ProfileBadge.vue';
const props = defineProps<{ const props = defineProps<{
@@ -28,7 +28,7 @@ const age = computed(() => {
}); });
const coverUrl = computed(() => const coverUrl = computed(() =>
props.profile.mediaUrls?.[currentImageIndex.value] || props.profile.avatarUrl || '', props.profile.media?.[currentImageIndex.value]?.path ?? '',
); );
// ─── Drag / swipe mechanics ─────────────────────────────────────────────────── // ─── Drag / swipe mechanics ───────────────────────────────────────────────────
@@ -113,7 +113,7 @@ function openProfile() {
} }
function nextImage() { function nextImage() {
if (currentImageIndex.value < (props.profile.mediaUrls?.length ?? 1) - 1) { if (currentImageIndex.value < (props.profile.media?.length ?? 1) - 1) {
currentImageIndex.value++; currentImageIndex.value++;
} }
} }
@@ -161,9 +161,9 @@ function onTouchEnd(e: TouchEvent) {
</div> </div>
<!-- Image gallery dots --> <!-- Image gallery dots -->
<div v-if="(profile.mediaUrls?.length ?? 0) > 1" class="feed-card__dots"> <div v-if="(profile.media?.length ?? 0) > 1" class="feed-card__dots">
<button <button
v-for="(_, i) in profile.mediaUrls" v-for="(_, i) in profile.media"
:key="i" :key="i"
class="feed-card__dot" class="feed-card__dot"
:class="{ 'feed-card__dot--active': i === currentImageIndex }" :class="{ 'feed-card__dot--active': i === currentImageIndex }"
@@ -192,7 +192,7 @@ function onTouchEnd(e: TouchEvent) {
</div> </div>
<h2 class="feed-card__name">{{ profile.name }}<span class="feed-card__age">, {{ age }}</span></h2> <h2 class="feed-card__name">{{ profile.name }}<span class="feed-card__age">, {{ age }}</span></h2>
<div v-if="profile.tags?.length" class="feed-card__tags"> <div v-if="profile.tags?.length" class="feed-card__tags">
<ProfileBadge v-for="tag in profile.tags?.slice(0, 4)" :key="tag.id" :label="tag.name" /> <ProfileBadge v-for="tag in profile.tags?.slice(0, 4)" :key="tag.id" :label="tag.value" />
</div> </div>
</div> </div>

View File

@@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
import { useFeedStore } from '@/stores/feed.store'; import { useFeed } from '@/composables/useFeed';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import FeedCard from './FeedCard.vue'; import FeedCard from './FeedCard.vue';
import EmptyState from '@/components/common/EmptyState.vue'; import EmptyState from '@/components/common/EmptyState.vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
const feedStore = useFeedStore(); const feedStore = useFeed();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const prefersReducedMotion = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; const prefersReducedMotion = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

View File

@@ -1,19 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, watch } from 'vue'; import { reactive, ref, watch } from 'vue';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { useFeedStore } from '@/stores/feed.store'; import { useFeed } from '@/composables/useFeed';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import type { District } from '@/stores/ui.store'; import type { District } from '@/composables/useUi';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
import AppDrawer from '@/components/common/AppDrawer.vue'; import AppDrawer from '@/components/common/AppDrawer.vue';
defineProps<{ open: boolean }>(); defineProps<{ open: boolean }>();
const emit = defineEmits<{ close: [] }>(); const emit = defineEmits<{ close: [] }>();
const uiStore = useUiStore(); const uiStore = useUi();
const feedStore = useFeedStore(); const feedStore = useFeed();
const authStore = useAuthStore(); const authStore = useAuth();
const filters = reactive({ const filters = reactive({
cityId: '', cityId: '',
@@ -103,7 +103,7 @@ function reset() {
class="filters__tag" class="filters__tag"
:class="{ 'filters__tag--active': filters.tagIds.includes(tag.id) }" :class="{ 'filters__tag--active': filters.tagIds.includes(tag.id) }"
@click="toggleTag(tag.id)" @click="toggleTag(tag.id)"
>{{ tag.name }}</button> >{{ tag.value }}</button>
</div> </div>
</div> </div>

View File

@@ -4,10 +4,10 @@ import { useRoute } from 'vue-router';
import TauriTitlebar from './TauriTitlebar.vue'; import TauriTitlebar from './TauriTitlebar.vue';
import SideNav from './SideNav.vue'; import SideNav from './SideNav.vue';
import BottomNav from './BottomNav.vue'; import BottomNav from './BottomNav.vue';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
const route = useRoute(); const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuth();
const isTauri = typeof window !== 'undefined' && !!window.__TAURI__; const isTauri = typeof window !== 'undefined' && !!window.__TAURI__;

View File

@@ -1,10 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store';
const route = useRoute(); const route = useRoute();
const authStore = useAuthStore();
const navItems = [ const navItems = [
{ path: '/feed', label: 'Лента', icon: 'grid' }, { path: '/feed', label: 'Лента', icon: 'grid' },

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
const route = useRoute(); const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
interface NavItem { interface NavItem {
name: string; name: string;

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
interface MediaItem { interface MediaItem {
@@ -12,7 +12,7 @@ interface MediaItem {
const props = defineProps<{ profileId: string; editable?: boolean }>(); const props = defineProps<{ profileId: string; editable?: boolean }>();
const emit = defineEmits<{ updated: [] }>(); const emit = defineEmits<{ updated: [] }>();
const uiStore = useUiStore(); const uiStore = useUi();
const items = ref<MediaItem[]>([]); const items = ref<MediaItem[]>([]);
const loading = ref(false); const loading = ref(false);
const uploading = ref(false); const uploading = ref(false);

View File

@@ -2,21 +2,21 @@
import { reactive, watch, ref } from 'vue'; import { reactive, watch, ref } from 'vue';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { required, helpers } from '@vuelidate/validators'; import { required, helpers } from '@vuelidate/validators';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useProfileStore } from '@/stores/profile.store'; import { useProfile } from '@/composables/useProfile';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import type { UserProfile } from '@/stores/auth.store'; import type { UserProfile } from '@/composables/useAuth';
import type { District } from '@/stores/ui.store'; import type { District } from '@/composables/useUi';
import AppInput from '@/components/common/AppInput.vue'; import AppInput from '@/components/common/AppInput.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
const props = defineProps<{ profile: UserProfile }>(); const props = defineProps<{ profile: UserProfile }>();
const emit = defineEmits<{ saved: [UserProfile]; cancel: [] }>(); const emit = defineEmits<{ saved: [UserProfile]; cancel: [] }>();
const uiStore = useUiStore(); const uiStore = useUi();
const authStore = useAuthStore(); const authStore = useAuth();
const profileStore = useProfileStore(); const profileStore = useProfile();
const form = reactive({ const form = reactive({
name: props.profile.name, name: props.profile.name,
@@ -28,7 +28,7 @@ const form = reactive({
nation: props.profile.nation ?? '', nation: props.profile.nation ?? '',
height: props.profile.height ?? undefined as number | undefined, height: props.profile.height ?? undefined as number | undefined,
weight: props.profile.weight ?? undefined as number | undefined, weight: props.profile.weight ?? undefined as number | undefined,
tagIds: [...(props.profile.tagIds ?? [])], tagIds: props.profile.tags?.map((t) => t.id) ?? [],
}); });
const districts = ref<District[]>([]); const districts = ref<District[]>([]);
@@ -136,7 +136,7 @@ async function save() {
class="profile-editor__tag" class="profile-editor__tag"
:class="{ 'profile-editor__tag--active': form.tagIds.includes(tag.id) }" :class="{ 'profile-editor__tag--active': form.tagIds.includes(tag.id) }"
@click="toggleTag(tag.id)" @click="toggleTag(tag.id)"
>{{ tag.name }}</button> >{{ tag.value }}</button>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import AppModal from '@/components/common/AppModal.vue'; import AppModal from '@/components/common/AppModal.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
@@ -14,8 +14,8 @@ const props = defineProps<{
const emit = defineEmits<{ close: [] }>(); const emit = defineEmits<{ close: [] }>();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const form = reactive({ description: '' }); const form = reactive({ description: '' });
const loading = ref(false); const loading = ref(false);

130
src/composables/useAuth.ts Normal file
View File

@@ -0,0 +1,130 @@
import { ref, computed, reactive } from 'vue';
import { apiClient, _setAccessToken, _clearAuth } from '@/api/client';
import type { LoginDto, RegisterDto, TagDto, MediaItemDto } from '@/api/api';
export interface UserProfile {
id: string;
userId: string;
name: string;
birthDate: string;
gender: 'male' | 'female';
cityId?: string | null;
districtId?: string | null;
description?: string | null;
nation?: string | null;
height?: number | null;
weight?: number | null;
activeChatId?: string | null;
tags: TagDto[];
media: MediaItemDto[];
}
export interface AuthUser {
id: string;
phone: string;
status: 'active' | 'banned' | 'pending';
roleId?: string | null;
role?: { id: string; name: string } | null;
profiles: UserProfile[];
}
const user = ref<AuthUser | null>(null);
const activeProfileId = ref<string | null>(null);
const isAuthenticated = computed(() => !!user.value);
const isAdmin = computed(() => user.value?.role?.name === 'admin');
const profiles = computed(() => user.value?.profiles ?? []);
const activeProfile = computed(() =>
profiles.value.find((p) => p.id === activeProfileId.value) ?? profiles.value[0] ?? null,
);
const hasProfiles = computed(() => profiles.value.length > 0);
async function login(dto: LoginDto) {
const res = await apiClient.api.authControllerLogin(dto) as unknown as {
accessToken: string;
refreshToken: string;
};
_setAccessToken(res.accessToken);
localStorage.setItem('refreshToken', res.refreshToken);
await fetchMe();
}
async function register(dto: RegisterDto) {
const res = await apiClient.api.authControllerRegister(dto) as unknown as {
accessToken: string;
refreshToken: string;
};
_setAccessToken(res.accessToken);
localStorage.setItem('refreshToken', res.refreshToken);
await fetchMe();
}
async function logout() {
try {
await apiClient.api.authControllerLogout();
} catch {
// ignore errors on logout
}
_clearAuth();
user.value = null;
activeProfileId.value = null;
}
async function fetchMe() {
const [meRes, profilesRes] = await Promise.all([
apiClient.api.usersControllerGetMe(),
apiClient.api.profilesControllerGetMyProfiles(),
]);
const fullProfiles = profilesRes as unknown as UserProfile[];
user.value = { ...meRes, profiles: fullProfiles } as unknown as AuthUser;
if (fullProfiles.length > 0 && !activeProfileId.value) {
activeProfileId.value = fullProfiles[0].id;
}
}
function setActiveProfile(profileId: string) {
activeProfileId.value = profileId;
}
function addProfile(profile: UserProfile) {
if (user.value) {
user.value.profiles.push(profile);
activeProfileId.value = profile.id;
}
}
function updateProfile(updated: UserProfile) {
if (user.value) {
const idx = user.value.profiles.findIndex((p) => p.id === updated.id);
if (idx !== -1) user.value.profiles[idx] = updated;
}
}
function removeProfile(profileId: string) {
if (user.value) {
user.value.profiles = user.value.profiles.filter((p) => p.id !== profileId);
if (activeProfileId.value === profileId) {
activeProfileId.value = user.value.profiles[0]?.id ?? null;
}
}
}
export function useAuth() {
return reactive({
user,
activeProfileId,
isAuthenticated,
isAdmin,
profiles,
activeProfile,
hasProfiles,
login,
register,
logout,
fetchMe,
setActiveProfile,
addProfile,
updateProfile,
removeProfile,
});
}

117
src/composables/useChat.ts Normal file
View File

@@ -0,0 +1,117 @@
import { ref, reactive } from 'vue';
import { apiClient } from '@/api/client';
import type { SendMessageDto } from '@/api/api';
export interface ChatProfile {
id: string;
name: string;
avatarUrl?: string;
}
export interface Chat {
id: string;
profile1Id: string;
profile2Id: string;
status: 'active' | 'closed';
partner?: ChatProfile;
lastMessage?: ChatMessage;
unreadCount?: number;
createdAt?: string;
}
export interface ChatMessage {
id: string;
chatId: string;
profileId: string;
text?: string;
mediaUrl?: string;
mediaType?: 'photo' | 'voice' | 'video';
createdAt: string;
}
// Polling interval — replace with WebSocket when backend supports it
const POLL_INTERVAL = 2000;
const chats = ref<Chat[]>([]);
const activeChat = ref<Chat | null>(null);
const messages = ref<ChatMessage[]>([]);
const pollingTimer = ref<ReturnType<typeof setInterval> | null>(null);
const loading = ref(false);
async function fetchChats(profileId: string) {
const res = await apiClient.api.chatControllerGetChats({ profileId }) as unknown as Chat[];
chats.value = res;
}
async function fetchMessages(chatId: string, profileId: string) {
loading.value = true;
try {
const res = await apiClient.api.chatControllerGetMessages({ chatId, profileId }) as unknown as ChatMessage[];
messages.value = res;
} finally {
loading.value = false;
}
}
async function sendMessage(chatId: string, profileId: string, dto: SendMessageDto) {
const res = await apiClient.api.chatControllerSendMessage({ chatId, profileId }, dto) as unknown as ChatMessage;
messages.value.push(res);
return res;
}
async function openChat(profileId: string, matchId: string) {
const res = await apiClient.api.chatControllerCreateChat({ profileId, matchId }) as unknown as Chat;
const existing = chats.value.findIndex((c) => c.id === res.id);
if (existing === -1) chats.value.unshift(res);
activeChat.value = res;
return res;
}
async function closeChat(chatId: string, profileId: string) {
await apiClient.api.chatControllerCloseChat({ chatId, profileId });
chats.value = chats.value.filter((c) => c.id !== chatId);
if (activeChat.value?.id === chatId) activeChat.value = null;
}
function startPolling(chatId: string, profileId: string) {
stopPolling();
// TODO: replace with WebSocket subscription
pollingTimer.value = setInterval(async () => {
try {
const res = await apiClient.api.chatControllerGetMessages({ chatId, profileId }) as unknown as ChatMessage[];
if (res.length > messages.value.length) {
messages.value = res;
}
} catch {
// polling errors are silent
}
}, POLL_INTERVAL);
}
function stopPolling() {
if (pollingTimer.value) {
clearInterval(pollingTimer.value);
pollingTimer.value = null;
}
}
function setActiveChat(chat: Chat | null) {
activeChat.value = chat;
}
export function useChat() {
return reactive({
chats,
activeChat,
messages,
loading,
fetchChats,
fetchMessages,
sendMessage,
openChat,
closeChat,
startPolling,
stopPolling,
setActiveChat,
});
}

View File

@@ -0,0 +1,66 @@
import { ref, reactive } from 'vue';
import { apiClient } from '@/api/client';
import type { FeedControllerGetFeedParams, TagDto, MediaItemDto } from '@/api/api';
export interface FeedProfile {
id: string;
name: string;
birthDate: string;
gender: 'male' | 'female';
cityId?: string | null;
cityName?: string;
districtId?: string | null;
description?: string | null;
nation?: string | null;
height?: number | null;
weight?: number | null;
tags?: TagDto[];
media?: MediaItemDto[];
}
const cards = ref<FeedProfile[]>([]);
const filters = reactive<Partial<FeedControllerGetFeedParams>>({});
const page = ref(1);
const hasMore = ref(true);
const searchPaused = ref(false);
const loading = ref(false);
async function fetchNextPage(profileId: string) {
if (loading.value || !hasMore.value) return;
loading.value = true;
try {
const res = await apiClient.api.feedControllerGetFeed({
profileId,
page: page.value,
limit: 20,
...filters,
}) as unknown as FeedProfile[];
if (page.value === 1) cards.value = res;
else cards.value.push(...res);
hasMore.value = res.length >= 20;
page.value++;
} finally {
loading.value = false;
}
}
function applyFilters(newFilters: Partial<FeedControllerGetFeedParams>) {
Object.assign(filters, newFilters);
reset();
}
function reset() {
cards.value = [];
page.value = 1;
hasMore.value = true;
}
function removeCard(profileId: string) {
cards.value = cards.value.filter((c) => c.id !== profileId);
}
export function useFeed() {
return reactive({ cards, filters, page, hasMore, searchPaused, loading, fetchNextPage, applyFilters, reset, removeCard });
}

View File

@@ -0,0 +1,39 @@
import { ref } from 'vue';
import { apiClient } from '@/api/client';
import type { CreateProfileDto, UpdateProfileDto } from '@/api/api';
import type { UserProfile } from './useAuth';
const currentProfile = ref<UserProfile | null>(null);
const loading = ref(false);
async function fetchProfile(profileId: string) {
loading.value = true;
try {
const res = await apiClient.api.profilesControllerFindOne(profileId) as unknown as UserProfile;
currentProfile.value = res;
return res;
} finally {
loading.value = false;
}
}
async function createProfile(dto: CreateProfileDto) {
const res = await apiClient.api.profilesControllerCreate(dto) as unknown as UserProfile;
currentProfile.value = res;
return res;
}
async function updateProfile(profileId: string, dto: UpdateProfileDto) {
const res = await apiClient.api.profilesControllerUpdate(profileId, dto) as unknown as UserProfile;
currentProfile.value = res;
return res;
}
async function deleteProfile(profileId: string) {
await apiClient.api.profilesControllerDelete(profileId);
currentProfile.value = null;
}
export function useProfile() {
return { currentProfile, loading, fetchProfile, createProfile, updateProfile, deleteProfile };
}

82
src/composables/useUi.ts Normal file
View File

@@ -0,0 +1,82 @@
import { ref, reactive } from 'vue';
export type ToastType = 'success' | 'error' | 'info' | 'warning';
export interface Toast {
id: string;
type: ToastType;
message: string;
duration?: number;
}
export interface Tag {
id: string;
value: string;
}
export interface City {
id: string;
name: string;
}
export interface District {
id: string;
name: string;
cityId: string;
}
export interface Greeting {
id: string;
text: string;
}
const toasts = ref<Toast[]>([]);
const sidebarExpanded = ref(false);
const tags = ref<Tag[]>([]);
const cities = ref<City[]>([]);
const districts = reactive<Record<string, District[]>>({});
const greetings = ref<Greeting[]>([]);
const referencesLoaded = ref(false);
function addToast(message: string, type: ToastType = 'info', duration = 4000) {
const id = `${Date.now()}-${Math.random()}`;
toasts.value.push({ id, type, message, duration });
if (duration > 0) {
setTimeout(() => removeToast(id), duration);
}
return id;
}
function removeToast(id: string) {
toasts.value = toasts.value.filter((t) => t.id !== id);
}
function setSidebarExpanded(value: boolean) {
sidebarExpanded.value = value;
}
function setTags(data: Tag[]) { tags.value = data; }
function setCities(data: City[]) { cities.value = data; }
function setDistricts(cityId: string, data: District[]) { districts[cityId] = data; }
function setGreetings(data: Greeting[]) { greetings.value = data; }
function setReferencesLoaded() { referencesLoaded.value = true; }
export function useUi() {
return reactive({
toasts,
sidebarExpanded,
tags,
cities,
districts,
greetings,
referencesLoaded,
addToast,
removeToast,
setSidebarExpanded,
setTags,
setCities,
setDistricts,
setGreetings,
setReferencesLoaded,
});
}

View File

@@ -1,13 +1,10 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { router } from './router'; import { router } from './router';
import App from './App.vue'; import App from './App.vue';
import '@/styles/tailwind.css'; import '@/styles/tailwind.css';
import '@/styles/main.scss'; import '@/styles/main.scss';
const pinia = createPinia();
const app = createApp(App); const app = createApp(App);
app.use(pinia);
app.use(router); app.use(router);
app.mount('#app'); app.mount('#app');

View File

@@ -1,5 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { _getAccessToken, _setAccessToken } from '@/api/client'; import { _getAccessToken, _setAccessToken } from '@/api/client';
import axios from 'axios'; import axios from 'axios';
@@ -45,7 +45,7 @@ export const router = createRouter({
let _initDone = false; let _initDone = false;
router.beforeEach(async (to, _from, next) => { router.beforeEach(async (to, _from, next) => {
const authStore = useAuthStore(); const authStore = useAuth();
// First navigation: try to restore session from localStorage refresh token // First navigation: try to restore session from localStorage refresh token
if (!_initDone) { if (!_initDone) {
@@ -53,12 +53,12 @@ router.beforeEach(async (to, _from, next) => {
const storedRefresh = localStorage.getItem('refreshToken'); const storedRefresh = localStorage.getItem('refreshToken');
if (storedRefresh && !_getAccessToken()) { if (storedRefresh && !_getAccessToken()) {
try { try {
const res = await axios.post<{ accessToken: string; refreshToken: string }>( const res = await axios.post<{ data: { accessToken: string; refreshToken: string } }>(
`${BASE_URL}/api/v1/auth/refresh`, `${BASE_URL}/api/v1/auth/refresh`,
{ refreshToken: storedRefresh }, { refreshToken: storedRefresh },
); );
_setAccessToken(res.data.accessToken); _setAccessToken(res.data.data.accessToken);
localStorage.setItem('refreshToken', res.data.refreshToken); localStorage.setItem('refreshToken', res.data.data.refreshToken);
await authStore.fetchMe(); await authStore.fetchMe();
} catch { } catch {
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');

View File

@@ -1,125 +0,0 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { apiClient, _setAccessToken, _clearAuth } from '@/api/client';
import type { LoginDto, RegisterDto } from '@/api/api';
export interface UserProfile {
id: string;
name: string;
birthDate: string;
gender: 'male' | 'female';
cityId?: string;
districtId?: string;
description?: string;
nation?: string;
height?: number;
weight?: number;
tagIds?: string[];
mediaUrls?: string[];
avatarUrl?: string;
}
export interface AuthUser {
id: string;
phone: string;
role: 'user' | 'admin';
isActive: boolean;
profiles: UserProfile[];
}
export const useAuthStore = defineStore('auth', () => {
const user = ref<AuthUser | null>(null);
const activeProfileId = ref<string | null>(null);
const isAuthenticated = computed(() => !!user.value);
const isAdmin = computed(() => user.value?.role === 'admin');
const profiles = computed(() => user.value?.profiles ?? []);
const activeProfile = computed(() =>
profiles.value.find((p) => p.id === activeProfileId.value) ?? profiles.value[0] ?? null,
);
const hasProfiles = computed(() => profiles.value.length > 0);
async function login(dto: LoginDto) {
const res = await apiClient.api.authControllerLogin(dto) as unknown as {
accessToken: string;
refreshToken: string;
};
_setAccessToken(res.accessToken);
localStorage.setItem('refreshToken', res.refreshToken);
await fetchMe();
}
async function register(dto: RegisterDto) {
const res = await apiClient.api.authControllerRegister(dto) as unknown as {
accessToken: string;
refreshToken: string;
};
_setAccessToken(res.accessToken);
localStorage.setItem('refreshToken', res.refreshToken);
await fetchMe();
}
async function logout() {
try {
await apiClient.api.authControllerLogout();
} catch {
// ignore errors on logout
}
_clearAuth();
user.value = null;
activeProfileId.value = null;
}
async function fetchMe() {
const res = await apiClient.api.usersControllerGetMe() as unknown as AuthUser;
user.value = res;
if (res.profiles?.length > 0 && !activeProfileId.value) {
activeProfileId.value = res.profiles[0].id;
}
}
function setActiveProfile(profileId: string) {
activeProfileId.value = profileId;
}
function addProfile(profile: UserProfile) {
if (user.value) {
user.value.profiles.push(profile);
activeProfileId.value = profile.id;
}
}
function updateProfile(updated: UserProfile) {
if (user.value) {
const idx = user.value.profiles.findIndex((p) => p.id === updated.id);
if (idx !== -1) user.value.profiles[idx] = updated;
}
}
function removeProfile(profileId: string) {
if (user.value) {
user.value.profiles = user.value.profiles.filter((p) => p.id !== profileId);
if (activeProfileId.value === profileId) {
activeProfileId.value = user.value.profiles[0]?.id ?? null;
}
}
}
return {
user,
activeProfileId,
isAuthenticated,
isAdmin,
profiles,
activeProfile,
hasProfiles,
login,
register,
logout,
fetchMe,
setActiveProfile,
addProfile,
updateProfile,
removeProfile,
};
});

View File

@@ -1,117 +0,0 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { apiClient } from '@/api/client';
import type { SendMessageDto } from '@/api/api';
export interface ChatProfile {
id: string;
name: string;
avatarUrl?: string;
}
export interface Chat {
id: string;
matchId: string;
isActive: boolean;
partner: ChatProfile;
lastMessage?: ChatMessage;
unreadCount: number;
createdAt: string;
}
export interface ChatMessage {
id: string;
chatId: string;
senderId: string;
text?: string;
mediaUrl?: string;
mediaType?: 'photo' | 'voice' | 'video';
createdAt: string;
}
// Polling interval — replace with WebSocket when backend supports it
const POLL_INTERVAL = 2000;
export const useChatStore = defineStore('chat', () => {
const chats = ref<Chat[]>([]);
const activeChat = ref<Chat | null>(null);
const messages = ref<ChatMessage[]>([]);
const pollingTimer = ref<ReturnType<typeof setInterval> | null>(null);
const loading = ref(false);
async function fetchChats(profileId: string) {
const res = await apiClient.api.chatControllerGetChats({ profileId }) as unknown as Chat[];
chats.value = res;
}
async function fetchMessages(chatId: string, profileId: string) {
loading.value = true;
try {
const res = await apiClient.api.chatControllerGetMessages({ chatId, profileId }) as unknown as ChatMessage[];
messages.value = res;
} finally {
loading.value = false;
}
}
async function sendMessage(chatId: string, profileId: string, dto: SendMessageDto) {
const res = await apiClient.api.chatControllerSendMessage({ chatId, profileId }, dto) as unknown as ChatMessage;
messages.value.push(res);
return res;
}
async function openChat(profileId: string, matchId: string) {
const res = await apiClient.api.chatControllerCreateChat({ profileId, matchId }) as unknown as Chat;
const existing = chats.value.findIndex((c) => c.id === res.id);
if (existing === -1) chats.value.unshift(res);
activeChat.value = res;
return res;
}
async function closeChat(chatId: string, profileId: string) {
await apiClient.api.chatControllerCloseChat({ chatId, profileId });
chats.value = chats.value.filter((c) => c.id !== chatId);
if (activeChat.value?.id === chatId) activeChat.value = null;
}
function startPolling(chatId: string, profileId: string) {
stopPolling();
// TODO: replace with WebSocket subscription
pollingTimer.value = setInterval(async () => {
try {
const res = await apiClient.api.chatControllerGetMessages({ chatId, profileId }) as unknown as ChatMessage[];
if (res.length > messages.value.length) {
messages.value = res;
}
} catch {
// polling errors are silent
}
}, POLL_INTERVAL);
}
function stopPolling() {
if (pollingTimer.value) {
clearInterval(pollingTimer.value);
pollingTimer.value = null;
}
}
function setActiveChat(chat: Chat | null) {
activeChat.value = chat;
}
return {
chats,
activeChat,
messages,
loading,
fetchChats,
fetchMessages,
sendMessage,
openChat,
closeChat,
startPolling,
stopPolling,
setActiveChat,
};
});

View File

@@ -1,71 +0,0 @@
import { defineStore } from 'pinia';
import { ref, reactive } from 'vue';
import { apiClient } from '@/api/client';
import type { FeedControllerGetFeedParams } from '@/api/api';
export interface FeedProfile {
id: string;
name: string;
birthDate: string;
age: number;
gender: 'male' | 'female';
cityId?: string;
cityName?: string;
districtId?: string;
districtName?: string;
description?: string;
nation?: string;
height?: number;
weight?: number;
tags?: Array<{ id: string; name: string }>;
mediaUrls: string[];
avatarUrl?: string;
}
export const useFeedStore = defineStore('feed', () => {
const cards = ref<FeedProfile[]>([]);
const filters = reactive<Partial<FeedControllerGetFeedParams>>({});
const page = ref(1);
const hasMore = ref(true);
const searchPaused = ref(false);
const loading = ref(false);
async function fetchNextPage(profileId: string) {
if (loading.value || !hasMore.value) return;
loading.value = true;
try {
const res = await apiClient.api.feedControllerGetFeed({
profileId,
page: page.value,
limit: 20,
...filters,
}) as unknown as { items: FeedProfile[]; hasMore: boolean; searchPaused?: boolean };
if (page.value === 1) cards.value = res.items;
else cards.value.push(...res.items);
hasMore.value = res.hasMore;
searchPaused.value = res.searchPaused ?? false;
page.value++;
} finally {
loading.value = false;
}
}
function applyFilters(newFilters: Partial<FeedControllerGetFeedParams>) {
Object.assign(filters, newFilters);
reset();
}
function reset() {
cards.value = [];
page.value = 1;
hasMore.value = true;
}
function removeCard(profileId: string) {
cards.value = cards.value.filter((c) => c.id !== profileId);
}
return { cards, filters, page, hasMore, searchPaused, loading, fetchNextPage, applyFilters, reset, removeCard };
});

View File

@@ -1,40 +0,0 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { apiClient } from '@/api/client';
import type { CreateProfileDto, UpdateProfileDto } from '@/api/api';
import type { UserProfile } from './auth.store';
export const useProfileStore = defineStore('profile', () => {
const currentProfile = ref<UserProfile | null>(null);
const loading = ref(false);
async function fetchProfile(profileId: string) {
loading.value = true;
try {
const res = await apiClient.api.profilesControllerFindOne(profileId) as unknown as UserProfile;
currentProfile.value = res;
return res;
} finally {
loading.value = false;
}
}
async function createProfile(dto: CreateProfileDto) {
const res = await apiClient.api.profilesControllerCreate(dto) as unknown as UserProfile;
currentProfile.value = res;
return res;
}
async function updateProfile(profileId: string, dto: UpdateProfileDto) {
const res = await apiClient.api.profilesControllerUpdate(profileId, dto) as unknown as UserProfile;
currentProfile.value = res;
return res;
}
async function deleteProfile(profileId: string) {
await apiClient.api.profilesControllerDelete(profileId);
currentProfile.value = null;
}
return { currentProfile, loading, fetchProfile, createProfile, updateProfile, deleteProfile };
});

View File

@@ -1,83 +0,0 @@
import { defineStore } from 'pinia';
import { ref, reactive } from 'vue';
export type ToastType = 'success' | 'error' | 'info' | 'warning';
export interface Toast {
id: string;
type: ToastType;
message: string;
duration?: number;
}
export interface Tag {
id: string;
name: string;
}
export interface City {
id: string;
name: string;
}
export interface District {
id: string;
name: string;
cityId: string;
}
export interface Greeting {
id: string;
text: string;
}
export const useUiStore = defineStore('ui', () => {
const toasts = ref<Toast[]>([]);
const sidebarExpanded = ref(false);
const tags = ref<Tag[]>([]);
const cities = ref<City[]>([]);
const districts = reactive<Record<string, District[]>>({});
const greetings = ref<Greeting[]>([]);
const referencesLoaded = ref(false);
function addToast(message: string, type: ToastType = 'info', duration = 4000) {
const id = `${Date.now()}-${Math.random()}`;
toasts.value.push({ id, type, message, duration });
if (duration > 0) {
setTimeout(() => removeToast(id), duration);
}
return id;
}
function removeToast(id: string) {
toasts.value = toasts.value.filter((t) => t.id !== id);
}
function setSidebarExpanded(value: boolean) {
sidebarExpanded.value = value;
}
function setTags(data: Tag[]) { tags.value = data; }
function setCities(data: City[]) { cities.value = data; }
function setDistricts(cityId: string, data: District[]) { districts[cityId] = data; }
function setGreetings(data: Greeting[]) { greetings.value = data; }
function setReferencesLoaded() { referencesLoaded.value = true; }
return {
toasts,
sidebarExpanded,
tags,
cities,
districts,
greetings,
referencesLoaded,
addToast,
removeToast,
setSidebarExpanded,
setTags,
setCities,
setDistricts,
setGreetings,
setReferencesLoaded,
};
});

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
@@ -17,7 +17,7 @@ interface Report {
reporterName?: string; reporterName?: string;
} }
const uiStore = useUiStore(); const uiStore = useUi();
const reports = ref<Report[]>([]); const reports = ref<Report[]>([]);
const loading = ref(false); const loading = ref(false);

View File

@@ -3,15 +3,15 @@ import { reactive, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { required, helpers } from '@vuelidate/validators'; import { required, helpers } from '@vuelidate/validators';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import AppInput from '@/components/common/AppInput.vue'; import AppInput from '@/components/common/AppInput.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const form = reactive({ phone: '', password: '' }); const form = reactive({ phone: '', password: '' });
const loading = ref(false); const loading = ref(false);

View File

@@ -3,14 +3,14 @@ import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { required, minLength, helpers } from '@vuelidate/validators'; import { required, minLength, helpers } from '@vuelidate/validators';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import AppInput from '@/components/common/AppInput.vue'; import AppInput from '@/components/common/AppInput.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const form = reactive({ phone: '', password: '', confirmPassword: '' }); const form = reactive({ phone: '', password: '', confirmPassword: '' });
const loading = ref(false); const loading = ref(false);

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'; import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useChatStore } from '@/stores/chat.store'; import { useChat } from '@/composables/useChat';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import type { ChatMessage } from '@/stores/chat.store'; import type { ChatMessage } from '@/composables/useChat';
import ChatBubble from '@/components/chat/ChatBubble.vue'; import ChatBubble from '@/components/chat/ChatBubble.vue';
import ChatInput from '@/components/chat/ChatInput.vue'; import ChatInput from '@/components/chat/ChatInput.vue';
import AppModal from '@/components/common/AppModal.vue'; import AppModal from '@/components/common/AppModal.vue';
@@ -13,9 +13,9 @@ import AppButton from '@/components/common/AppButton.vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
const route = useRoute(); const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuth();
const chatStore = useChatStore(); const chatStore = useChat();
const uiStore = useUiStore(); const uiStore = useUi();
const chatId = route.params.chatId as string; const chatId = route.params.chatId as string;
const profileId = computed(() => authStore.activeProfile?.id ?? ''); const profileId = computed(() => authStore.activeProfile?.id ?? '');
@@ -23,7 +23,7 @@ const messagesEnd = ref<HTMLElement | null>(null);
const confirmClose = ref(false); const confirmClose = ref(false);
const chat = computed(() => chatStore.chats.find((c) => c.id === chatId)); const chat = computed(() => chatStore.chats.find((c) => c.id === chatId));
const isLocked = computed(() => chat.value && !chat.value.isActive); const isLocked = computed(() => chat.value?.status === 'closed');
// Group messages by date // Group messages by date
const groupedMessages = computed(() => { const groupedMessages = computed(() => {
@@ -66,12 +66,14 @@ async function send(text: string, mediaUrl?: string, mediaType?: 'photo' | 'voic
} }
} }
function goBack() { window.history.back(); }
async function doCloseChat() { async function doCloseChat() {
if (!profileId.value) return; if (!profileId.value) return;
try { try {
await chatStore.closeChat(chatId, profileId.value); await chatStore.closeChat(chatId, profileId.value);
confirmClose.value = false; confirmClose.value = false;
history.back(); goBack();
} catch { } catch {
uiStore.addToast('Не удалось закрыть чат', 'error'); uiStore.addToast('Не удалось закрыть чат', 'error');
} }
@@ -82,20 +84,20 @@ async function doCloseChat() {
<div class="chat-room"> <div class="chat-room">
<!-- Header --> <!-- Header -->
<header class="chat-room__header"> <header class="chat-room__header">
<button class="chat-room__back" @click="history.back()" aria-label="Назад"> <button class="chat-room__back" @click="goBack()" aria-label="Назад">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="20" height="20"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="20" height="20">
<path d="M19 12H5M12 5l-7 7 7 7"/> <path d="M19 12H5M12 5l-7 7 7 7"/>
</svg> </svg>
</button> </button>
<div class="chat-room__partner"> <div class="chat-room__partner">
<img <img
v-if="chat?.partner.avatarUrl" v-if="chat?.partner?.avatarUrl"
:src="chat.partner.avatarUrl" :src="chat.partner.avatarUrl"
class="chat-room__avatar" class="chat-room__avatar"
:alt="chat.partner.name" :alt="chat.partner?.name"
/> />
<div v-else class="chat-room__avatar chat-room__avatar--placeholder" /> <div v-else class="chat-room__avatar chat-room__avatar--placeholder" />
<span class="chat-room__name">{{ chat?.partner.name ?? 'Чат' }}</span> <span class="chat-room__name">{{ chat?.partner?.name ?? 'Чат' }}</span>
</div> </div>
<button <button
class="chat-room__close-btn" class="chat-room__close-btn"
@@ -135,7 +137,7 @@ async function doCloseChat() {
v-for="msg in group.messages" v-for="msg in group.messages"
:key="msg.id" :key="msg.id"
:message="msg" :message="msg"
:is-mine="msg.senderId === authStore.activeProfile?.id" :is-mine="msg.profileId === authStore.activeProfile?.id"
/> />
</div> </div>
<div ref="messagesEnd" /> <div ref="messagesEnd" />

View File

@@ -1,14 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useChatStore } from '@/stores/chat.store'; import { useChat } from '@/composables/useChat';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import EmptyState from '@/components/common/EmptyState.vue'; import EmptyState from '@/components/common/EmptyState.vue';
const authStore = useAuthStore(); const authStore = useAuth();
const chatStore = useChatStore(); const chatStore = useChat();
const uiStore = useUiStore(); const uiStore = useUi();
const loading = ref(false); const loading = ref(false);
onMounted(async () => { onMounted(async () => {
@@ -55,13 +55,13 @@ function formatTime(dateStr: string) {
<RouterLink <RouterLink
:to="`/chats/${chat.id}`" :to="`/chats/${chat.id}`"
class="chat-item" class="chat-item"
:class="{ 'chat-item--inactive': !chat.isActive }" :class="{ 'chat-item--inactive': chat.status === 'closed' }"
> >
<div class="chat-item__avatar-wrap"> <div class="chat-item__avatar-wrap">
<img <img
v-if="chat.partner.avatarUrl" v-if="chat.partner?.avatarUrl"
:src="chat.partner.avatarUrl" :src="chat.partner.avatarUrl"
:alt="chat.partner.name" :alt="chat.partner?.name"
class="chat-item__avatar" class="chat-item__avatar"
/> />
<div v-else class="chat-item__avatar chat-item__avatar--placeholder"> <div v-else class="chat-item__avatar chat-item__avatar--placeholder">
@@ -70,7 +70,7 @@ function formatTime(dateStr: string) {
</svg> </svg>
</div> </div>
<!-- Lock icon for inactive chats --> <!-- Lock icon for inactive chats -->
<div v-if="!chat.isActive" class="chat-item__lock" aria-label="Чат неактивен"> <div v-if="chat.status === 'closed'" class="chat-item__lock" aria-label="Чат неактивен">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" width="12" height="12"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" width="12" height="12">
<rect x="3" y="7" width="10" height="8" rx="1"/> <rect x="3" y="7" width="10" height="8" rx="1"/>
<path d="M5 7V5a3 3 0 0 1 6 0v2"/> <path d="M5 7V5a3 3 0 0 1 6 0v2"/>
@@ -80,7 +80,7 @@ function formatTime(dateStr: string) {
<div class="chat-item__content"> <div class="chat-item__content">
<div class="chat-item__top"> <div class="chat-item__top">
<span class="chat-item__name">{{ chat.partner.name }}</span> <span class="chat-item__name">{{ chat.partner?.name }}</span>
<span v-if="chat.lastMessage" class="chat-item__time meta"> <span v-if="chat.lastMessage" class="chat-item__time meta">
{{ formatTime(chat.lastMessage.createdAt) }} {{ formatTime(chat.lastMessage.createdAt) }}
</span> </span>
@@ -91,7 +91,7 @@ function formatTime(dateStr: string) {
<p v-else class="chat-item__preview chat-item__preview--empty">Начните переписку</p> <p v-else class="chat-item__preview chat-item__preview--empty">Начните переписку</p>
</div> </div>
<div v-if="chat.unreadCount > 0" class="chat-item__badge">{{ chat.unreadCount }}</div> <div v-if="(chat.unreadCount ?? 0) > 0" class="chat-item__badge">{{ chat.unreadCount }}</div>
</RouterLink> </RouterLink>
</li> </li>
</ul> </ul>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
import AppModal from '@/components/common/AppModal.vue'; import AppModal from '@/components/common/AppModal.vue';
@@ -22,8 +22,8 @@ interface DateItem {
isIncoming: boolean; isIncoming: boolean;
} }
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const dates = ref<DateItem[]>([]); const dates = ref<DateItem[]>([]);
const statuses = ref<DateStatus[]>([]); const statuses = ref<DateStatus[]>([]);

View File

@@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useFeedStore } from '@/stores/feed.store'; import { useFeed } from '@/composables/useFeed';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import FeedCardStack from '@/components/feed/FeedCardStack.vue'; import FeedCardStack from '@/components/feed/FeedCardStack.vue';
import FeedFilters from '@/components/feed/FeedFilters.vue'; import FeedFilters from '@/components/feed/FeedFilters.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
const feedStore = useFeedStore(); const feedStore = useFeed();
const authStore = useAuthStore(); const authStore = useAuth();
const filtersOpen = ref(false); const filtersOpen = ref(false);
const viewMode = ref<'stack' | 'scroll'>('stack'); const viewMode = ref<'stack' | 'scroll'>('stack');
@@ -83,8 +83,8 @@ onMounted(() => {
> >
<RouterLink :to="`/profile/${profile.id}`" class="feed-grid__link"> <RouterLink :to="`/profile/${profile.id}`" class="feed-grid__link">
<img <img
v-if="profile.avatarUrl" v-if="profile.media?.[0]?.path"
:src="profile.avatarUrl" :src="profile.media[0].path"
:alt="profile.name" :alt="profile.name"
class="feed-grid__img" class="feed-grid__img"
/> />

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useChatStore } from '@/stores/chat.store'; import { useChat } from '@/composables/useChat';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
@@ -23,9 +23,9 @@ interface Match {
hasChat: boolean; hasChat: boolean;
} }
const authStore = useAuthStore(); const authStore = useAuth();
const chatStore = useChatStore(); const chatStore = useChat();
const uiStore = useUiStore(); const uiStore = useUi();
const router = useRouter(); const router = useRouter();
const matches = ref<Match[]>([]); const matches = ref<Match[]>([]);

View File

@@ -3,35 +3,35 @@ import { reactive, ref, computed, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { required, helpers } from '@vuelidate/validators'; import { required, helpers } from '@vuelidate/validators';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import type { CreateProfileDto } from '@/api/api'; import type { CreateProfileDto } from '@/api/api';
import type { UserProfile } from '@/stores/auth.store'; import type { UserProfile } from '@/composables/useAuth';
import type { District } from '@/stores/ui.store'; import type { District } from '@/composables/useUi';
import AppInput from '@/components/common/AppInput.vue'; import AppInput from '@/components/common/AppInput.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const step = ref(1); const step = ref(1);
const totalSteps = 4; const totalSteps = 4;
const loading = ref(false); const loading = ref(false);
const form = reactive<CreateProfileDto & { confirmStep?: number }>({ const form = reactive({
name: '', name: '',
birthDate: '', birthDate: '',
gender: 'female', gender: 'female' as 'female' | 'male',
cityId: '', cityId: '',
districtId: '', districtId: '',
description: '', description: '',
nation: '', nation: '',
height: undefined, height: undefined as number | undefined | null,
weight: undefined, weight: undefined as number | undefined | null,
tagIds: [], tagIds: [] as string[],
}); });
const selectedTags = ref<string[]>([]); const selectedTags = ref<string[]>([]);
@@ -85,7 +85,7 @@ async function finish() {
loading.value = true; loading.value = true;
try { try {
form.tagIds = selectedTags.value; form.tagIds = selectedTags.value;
const profile = await apiClient.api.profilesControllerCreate(form) as unknown as UserProfile; const profile = await apiClient.api.profilesControllerCreate(form as unknown as CreateProfileDto) as unknown as UserProfile;
authStore.addProfile(profile); authStore.addProfile(profile);
uiStore.addToast('Профиль создан', 'success'); uiStore.addToast('Профиль создан', 'success');
router.replace('/feed'); router.replace('/feed');
@@ -207,7 +207,7 @@ function skip() {
:class="{ 'setup__tag--active': selectedTags.includes(tag.id) }" :class="{ 'setup__tag--active': selectedTags.includes(tag.id) }"
@click="toggleTag(tag.id)" @click="toggleTag(tag.id)"
> >
{{ tag.name }} {{ tag.value }}
</button> </button>
</div> </div>
<p v-if="uiStore.tags.length === 0" class="setup__hint setup__hint--muted">Теги загружаются...</p> <p v-if="uiStore.tags.length === 0" class="setup__hint setup__hint--muted">Теги загружаются...</p>

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import type { UserProfile } from '@/stores/auth.store'; import type { UserProfile } from '@/composables/useAuth';
import ProfileEditor from '@/components/profile/ProfileEditor.vue'; import ProfileEditor from '@/components/profile/ProfileEditor.vue';
import MediaGallery from '@/components/profile/MediaGallery.vue'; import MediaGallery from '@/components/profile/MediaGallery.vue';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
import AppModal from '@/components/common/AppModal.vue'; import AppModal from '@/components/common/AppModal.vue';
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const editing = ref(false); const editing = ref(false);
const confirmDelete = ref(false); const confirmDelete = ref(false);
@@ -41,12 +41,12 @@ function logout() {
authStore.logout(); authStore.logout();
} }
function cityName(cityId?: string) { function cityName(cityId?: string | null) {
return uiStore.cities.find((c) => c.id === cityId)?.name ?? ''; return uiStore.cities.find((c) => c.id === cityId)?.name ?? '';
} }
function tagNames(tagIds?: string[]) { function tagValues(tags?: Array<{ id: string; value: string }>) {
return (tagIds ?? []).map((id) => uiStore.tags.find((t) => t.id === id)?.name ?? id); return (tags ?? []).map((t) => t.value);
} }
function calcAge(birthDate: string) { function calcAge(birthDate: string) {
@@ -66,8 +66,8 @@ function calcAge(birthDate: string) {
<div class="my-profile__hero"> <div class="my-profile__hero">
<div class="my-profile__avatar-wrap"> <div class="my-profile__avatar-wrap">
<img <img
v-if="profile.avatarUrl" v-if="profile.media?.[0]?.path"
:src="profile.avatarUrl" :src="profile.media[0].path"
:alt="profile.name" :alt="profile.name"
class="my-profile__avatar" class="my-profile__avatar"
/> />
@@ -113,10 +113,10 @@ function calcAge(birthDate: string) {
</div> </div>
<!-- Tags --> <!-- Tags -->
<div v-if="profile.tagIds?.length" class="my-profile__section"> <div v-if="profile.tags?.length" class="my-profile__section">
<h3 class="my-profile__section-title">Интересы</h3> <h3 class="my-profile__section-title">Интересы</h3>
<div class="my-profile__tags"> <div class="my-profile__tags">
<span v-for="name in tagNames(profile.tagIds)" :key="name" class="my-profile__tag">{{ name }}</span> <span v-for="name in tagValues(profile.tags)" :key="name" class="my-profile__tag">{{ name }}</span>
</div> </div>
</div> </div>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store'; import { useAuth } from '@/composables/useAuth';
import { useUiStore } from '@/stores/ui.store'; import { useUi } from '@/composables/useUi';
import { useChatStore } from '@/stores/chat.store'; import { useChat } from '@/composables/useChat';
import { apiClient } from '@/api/client'; import { apiClient } from '@/api/client';
import type { UserProfile } from '@/stores/auth.store'; import type { UserProfile } from '@/composables/useAuth';
import AppButton from '@/components/common/AppButton.vue'; import AppButton from '@/components/common/AppButton.vue';
import AppModal from '@/components/common/AppModal.vue'; import AppModal from '@/components/common/AppModal.vue';
import ReportModal from '@/components/reports/ReportModal.vue'; import ReportModal from '@/components/reports/ReportModal.vue';
@@ -13,9 +13,9 @@ import DateProposalForm from '@/components/dates/DateProposalForm.vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
const route = useRoute(); const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuth();
const uiStore = useUiStore(); const uiStore = useUi();
const chatStore = useChatStore(); const chatStore = useChat();
const profileId = route.params.profileId as string; const profileId = route.params.profileId as string;
const profile = ref<UserProfile | null>(null); const profile = ref<UserProfile | null>(null);
@@ -44,13 +44,18 @@ const age = computed(() => {
return a; return a;
}); });
const cityName = computed(() => uiStore.cities.find((c) => c.id === profile.value?.cityId)?.name ?? ''); const cityName = computed(() => {
const tagNames = computed(() => (profile.value?.tagIds ?? []).map((id) => uiStore.tags.find((t) => t.id === id)?.name ?? id)); const cid = profile.value?.cityId;
return cid ? uiStore.cities.find((c) => c.id === cid)?.name ?? '' : '';
});
const tagNames = computed(() => profile.value?.tags?.map((t) => t.value) ?? []);
const currentImageIndex = ref(0); const currentImageIndex = ref(0);
const isOwnProfile = computed(() => const isOwnProfile = computed(() =>
authStore.profiles.some((p) => p.id === profileId), authStore.profiles.some((p) => p.id === profileId),
); );
function goBack() { window.history.back(); }
</script> </script>
<template> <template>
@@ -63,8 +68,8 @@ const isOwnProfile = computed(() =>
<!-- Image strip --> <!-- Image strip -->
<div class="profile-detail__cover"> <div class="profile-detail__cover">
<img <img
v-if="profile.mediaUrls?.[currentImageIndex]" v-if="profile.media?.[currentImageIndex]?.path"
:src="profile.mediaUrls[currentImageIndex]" :src="profile.media[currentImageIndex].path"
:alt="profile.name" :alt="profile.name"
class="profile-detail__cover-img" class="profile-detail__cover-img"
/> />
@@ -78,14 +83,14 @@ const isOwnProfile = computed(() =>
aria-label="Предыдущее фото" aria-label="Предыдущее фото"
></button> ></button>
<button <button
v-if="currentImageIndex < (profile.mediaUrls?.length ?? 1) - 1" v-if="currentImageIndex < (profile.media?.length ?? 1) - 1"
class="profile-detail__img-nav profile-detail__img-nav--next" class="profile-detail__img-nav profile-detail__img-nav--next"
@click="currentImageIndex++" @click="currentImageIndex++"
aria-label="Следующее фото" aria-label="Следующее фото"
></button> ></button>
<!-- Back button --> <!-- Back button -->
<button class="profile-detail__back" @click="history.back()" aria-label="Назад"> <button class="profile-detail__back" @click="goBack()" aria-label="Назад">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
<path d="M19 12H5M12 5l-7 7 7 7"/> <path d="M19 12H5M12 5l-7 7 7 7"/>
</svg> </svg>