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:
@@ -5,11 +5,11 @@ Vue 3 + Vite + Tauri v2. Работает как PWA в браузере и ка
|
||||
## Стек
|
||||
|
||||
| Слой | Технология |
|
||||
|---|---|
|
||||
|---|-------------------------------------------------|
|
||||
| UI framework | Vue 3 (Composition API, `<script setup>`) |
|
||||
| Build tool | Vite 6 |
|
||||
| Desktop shell | Tauri v2 |
|
||||
| State management | Pinia |
|
||||
| State management | Composables | |
|
||||
| Routing | Vue Router 4 |
|
||||
| HTTP client | Axios (сгенерированный клиент `src/api/api.ts`) |
|
||||
| Форм-валидация | Vuelidate |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"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": {
|
||||
"@floating-ui/vue": "^1.1.5",
|
||||
@@ -22,7 +22,6 @@
|
||||
"esbuild": "^0.28.0",
|
||||
"gsap": "^3.12.5",
|
||||
"leaflet": "^1.9.4",
|
||||
"pinia": "^2.2.2",
|
||||
"vue": "^3.5.6",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -43,9 +43,6 @@ importers:
|
||||
leaflet:
|
||||
specifier: ^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:
|
||||
specifier: ^3.5.6
|
||||
version: 3.5.35(typescript@5.9.3)
|
||||
@@ -1278,15 +1275,6 @@ packages:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
@@ -2347,16 +2335,6 @@ snapshots:
|
||||
|
||||
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@8.5.15:
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import AppShell from '@/components/layout/AppShell.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 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() {
|
||||
if (uiStore.referencesLoaded) return;
|
||||
|
||||
355
src/api/api.ts
355
src/api/api.ts
@@ -16,6 +16,13 @@ export interface RegisterDto {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface TokensResponseDto {
|
||||
/** @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." */
|
||||
accessToken: string;
|
||||
/** @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." */
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface LoginDto {
|
||||
/** @example "+79991234567" */
|
||||
phone: string;
|
||||
@@ -23,10 +30,48 @@ export interface LoginDto {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface MessageResponseDto {
|
||||
/** @example "Operation successful" */
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenDto {
|
||||
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 {
|
||||
name: string;
|
||||
/** @example "1995-06-15" */
|
||||
@@ -41,6 +86,52 @@ export interface CreateProfileDto {
|
||||
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 {
|
||||
name?: string;
|
||||
/** @example "1995-06-15" */
|
||||
@@ -63,6 +154,26 @@ export interface CreateLikeDto {
|
||||
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 {
|
||||
/** Your profile ID */
|
||||
profileId: string;
|
||||
@@ -70,6 +181,23 @@ export interface CreateChatDto {
|
||||
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 {
|
||||
text?: string;
|
||||
mediaUrl?: string;
|
||||
@@ -87,6 +215,26 @@ export interface CreateDateDto {
|
||||
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 {
|
||||
statusId: string;
|
||||
}
|
||||
@@ -99,6 +247,56 @@ export interface CreateReportDto {
|
||||
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 {
|
||||
type: string;
|
||||
profileId: string;
|
||||
@@ -129,6 +327,10 @@ export interface ChatControllerGetChatsParams {
|
||||
|
||||
export interface ChatControllerGetMessagesParams {
|
||||
profileId: string;
|
||||
/** @default 50 */
|
||||
limit?: number;
|
||||
/** @default 1 */
|
||||
page?: number;
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
@@ -151,6 +353,16 @@ export interface DatesControllerUpdateStatusParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface TagsControllerCreatePayload {
|
||||
/** @example "Спорт" */
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface GreetingsControllerCreatePayload {
|
||||
/** @example "Привет!" */
|
||||
text: string;
|
||||
}
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, HeadersDefaults, ResponseType } from "axios";
|
||||
|
||||
export type QueryParamsType = Record<string | number, any>;
|
||||
@@ -307,11 +519,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/auth/register
|
||||
*/
|
||||
authControllerRegister: (data: RegisterDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<TokensResponseDto, any>({
|
||||
path: `/api/v1/auth/register`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -324,11 +537,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/auth/login
|
||||
*/
|
||||
authControllerLogin: (data: LoginDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<TokensResponseDto, any>({
|
||||
path: `/api/v1/auth/login`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -342,10 +556,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
authControllerLogout: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/auth/logout`,
|
||||
method: "POST",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -358,11 +573,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/auth/refresh
|
||||
*/
|
||||
authControllerRefresh: (data: RefreshTokenDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<TokensResponseDto, any>({
|
||||
path: `/api/v1/auth/refresh`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -375,11 +591,14 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/auth/fcm-token
|
||||
* @secure
|
||||
*/
|
||||
authControllerUpdateFcmToken: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
authControllerUpdateFcmToken: (data: AuthControllerUpdateFcmTokenPayload, params: RequestParams = {}) =>
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/auth/fcm-token`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -393,10 +612,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
usersControllerGetMe: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MeResponseDto, any>({
|
||||
path: `/api/v1/users/me`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -410,10 +630,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
usersControllerFindOne: (id: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<UserResponseDto, any>({
|
||||
path: `/api/v1/users/${id}`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -427,10 +648,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
usersControllerBan: (id: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/users/${id}/ban`,
|
||||
method: "PATCH",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -444,10 +666,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
usersControllerActivate: (id: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/users/${id}/activate`,
|
||||
method: "PATCH",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -461,12 +684,13 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
profilesControllerCreate: (data: CreateProfileDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ProfileResponseDto, any>({
|
||||
path: `/api/v1/profiles`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -480,10 +704,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
profilesControllerGetMyProfiles: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ProfileResponseDto[], any>({
|
||||
path: `/api/v1/profiles/my`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -497,12 +722,13 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
profilesControllerUpdate: (profileId: string, data: UpdateProfileDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ProfileResponseDto, any>({
|
||||
path: `/api/v1/profiles/${profileId}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -516,10 +742,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
profilesControllerFindOne: (profileId: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ProfileResponseDto, any>({
|
||||
path: `/api/v1/profiles/${profileId}`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -533,10 +760,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
profilesControllerDelete: (profileId: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/profiles/${profileId}`,
|
||||
method: "DELETE",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -550,11 +778,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
mediaControllerUpload: ({ profileId, ...query }: MediaControllerUploadParams, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MediaItemDto, any>({
|
||||
path: `/api/v1/profiles/${profileId}/media/upload`,
|
||||
method: "POST",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -568,10 +797,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
mediaControllerGetMedia: (profileId: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MediaItemDto[], any>({
|
||||
path: `/api/v1/profiles/${profileId}/media`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -585,10 +815,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
mediaControllerDeleteMedia: (mediaId: string, profileId: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/profiles/${profileId}/media/${mediaId}`,
|
||||
method: "DELETE",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -602,11 +833,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
feedControllerGetFeed: (query: FeedControllerGetFeedParams, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ProfileResponseDto[], any>({
|
||||
path: `/api/v1/feed`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -620,12 +852,13 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
likesControllerCreateLike: (data: CreateLikeDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<CreateLikeResponseDto, any>({
|
||||
path: `/api/v1/likes`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -639,11 +872,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
likesControllerGetMyMatches: (query: LikesControllerGetMyMatchesParams, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MatchDto[], any>({
|
||||
path: `/api/v1/likes/matches`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -657,12 +891,13 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
chatControllerCreateChat: (data: CreateChatDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ChatDto, any>({
|
||||
path: `/api/v1/chats`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -676,11 +911,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
chatControllerGetChats: (query: ChatControllerGetChatsParams, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ChatDto[], any>({
|
||||
path: `/api/v1/chats`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -694,11 +930,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
chatControllerGetMessages: ({ chatId, ...query }: ChatControllerGetMessagesParams, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageDto[], any>({
|
||||
path: `/api/v1/chats/${chatId}/messages`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -716,13 +953,14 @@ export class Api<SecurityDataType extends unknown> {
|
||||
data: SendMessageDto,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageDto, any>({
|
||||
path: `/api/v1/chats/${chatId}/messages`,
|
||||
method: "POST",
|
||||
query: query,
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -736,11 +974,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
chatControllerCloseChat: ({ chatId, ...query }: ChatControllerCloseChatParams, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/chats/${chatId}`,
|
||||
method: "DELETE",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -754,12 +993,13 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
datesControllerCreate: (data: CreateDateDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<DateDto, any>({
|
||||
path: `/api/v1/dates`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -773,11 +1013,12 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
datesControllerGetDates: (query: DatesControllerGetDatesParams, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<DateWithStatusDto[], any>({
|
||||
path: `/api/v1/dates`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -795,13 +1036,14 @@ export class Api<SecurityDataType extends unknown> {
|
||||
data: UpdateDateStatusDto,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<DateDto, any>({
|
||||
path: `/api/v1/dates/${id}/status`,
|
||||
method: "PATCH",
|
||||
query: query,
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -815,10 +1057,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
datesControllerGetStatuses: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<DateStatusDto[], any>({
|
||||
path: `/api/v1/dates/statuses`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -832,12 +1075,13 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
reportsControllerCreate: (data: CreateReportDto, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ReportDto, any>({
|
||||
path: `/api/v1/reports`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -851,10 +1095,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
reportsControllerGetAll: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<ReportDto[], any>({
|
||||
path: `/api/v1/reports`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -867,9 +1112,10 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request GET:/api/v1/tags
|
||||
*/
|
||||
tagsControllerFindAll: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<TagResponseDto[], any>({
|
||||
path: `/api/v1/tags`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -882,11 +1128,14 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/tags
|
||||
* @secure
|
||||
*/
|
||||
tagsControllerCreate: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
tagsControllerCreate: (data: TagsControllerCreatePayload, params: RequestParams = {}) =>
|
||||
this.http.request<TagResponseDto, any>({
|
||||
path: `/api/v1/tags`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -900,10 +1149,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
tagsControllerDelete: (id: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/tags/${id}`,
|
||||
method: "DELETE",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -916,9 +1166,10 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request GET:/api/v1/cities
|
||||
*/
|
||||
citiesControllerFindAll: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<CityResponseDto[], any>({
|
||||
path: `/api/v1/cities`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -931,11 +1182,14 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/cities
|
||||
* @secure
|
||||
*/
|
||||
citiesControllerCreateCity: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
citiesControllerCreateCity: (data: CreateCityDto, params: RequestParams = {}) =>
|
||||
this.http.request<CityResponseDto, any>({
|
||||
path: `/api/v1/cities`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -948,9 +1202,10 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request GET:/api/v1/cities/{cityId}/districts
|
||||
*/
|
||||
citiesControllerFindDistricts: (cityId: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<DistrictResponseDto[], any>({
|
||||
path: `/api/v1/cities/${cityId}/districts`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -963,11 +1218,14 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/cities/{cityId}/districts
|
||||
* @secure
|
||||
*/
|
||||
citiesControllerCreateDistrict: (cityId: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
citiesControllerCreateDistrict: (cityId: string, data: CreateDistrictDto, params: RequestParams = {}) =>
|
||||
this.http.request<DistrictResponseDto, any>({
|
||||
path: `/api/v1/cities/${cityId}/districts`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -980,9 +1238,10 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request GET:/api/v1/greetings
|
||||
*/
|
||||
greetingsControllerFindAll: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<GreetingDto[], any>({
|
||||
path: `/api/v1/greetings`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -995,11 +1254,14 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @request POST:/api/v1/greetings
|
||||
* @secure
|
||||
*/
|
||||
greetingsControllerCreate: (params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
greetingsControllerCreate: (data: GreetingsControllerCreatePayload, params: RequestParams = {}) =>
|
||||
this.http.request<GreetingDto, any>({
|
||||
path: `/api/v1/greetings`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
|
||||
@@ -1013,10 +1275,11 @@ export class Api<SecurityDataType extends unknown> {
|
||||
* @secure
|
||||
*/
|
||||
greetingsControllerDelete: (id: string, params: RequestParams = {}) =>
|
||||
this.http.request<void, any>({
|
||||
this.http.request<MessageResponseDto, any>({
|
||||
path: `/api/v1/greetings/${id}`,
|
||||
method: "DELETE",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { ChatMessage } from '@/stores/chat.store';
|
||||
import type { ChatMessage } from '@/composables/useChat';
|
||||
import MediaMessage from './MediaMessage.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
const uiStore = useUiStore();
|
||||
import { useUi } from '@/composables/useUi';
|
||||
const uiStore = useUi();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { apiClient } from '@/api/client';
|
||||
import MapPicker from './MapPicker.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 emit = defineEmits<{ close: []; created: [] }>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const form = reactive({
|
||||
time: '',
|
||||
|
||||
@@ -16,7 +16,7 @@ onMounted(async () => {
|
||||
await import('leaflet/dist/leaflet.css');
|
||||
|
||||
// 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({
|
||||
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',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { gsap } from 'gsap';
|
||||
import type { FeedProfile } from '@/stores/feed.store';
|
||||
import type { FeedProfile } from '@/composables/useFeed';
|
||||
import ProfileBadge from './ProfileBadge.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -28,7 +28,7 @@ const age = computed(() => {
|
||||
});
|
||||
|
||||
const coverUrl = computed(() =>
|
||||
props.profile.mediaUrls?.[currentImageIndex.value] || props.profile.avatarUrl || '',
|
||||
props.profile.media?.[currentImageIndex.value]?.path ?? '',
|
||||
);
|
||||
|
||||
// ─── Drag / swipe mechanics ───────────────────────────────────────────────────
|
||||
@@ -113,7 +113,7 @@ function openProfile() {
|
||||
}
|
||||
|
||||
function nextImage() {
|
||||
if (currentImageIndex.value < (props.profile.mediaUrls?.length ?? 1) - 1) {
|
||||
if (currentImageIndex.value < (props.profile.media?.length ?? 1) - 1) {
|
||||
currentImageIndex.value++;
|
||||
}
|
||||
}
|
||||
@@ -161,9 +161,9 @@ function onTouchEnd(e: TouchEvent) {
|
||||
</div>
|
||||
|
||||
<!-- 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
|
||||
v-for="(_, i) in profile.mediaUrls"
|
||||
v-for="(_, i) in profile.media"
|
||||
:key="i"
|
||||
class="feed-card__dot"
|
||||
:class="{ 'feed-card__dot--active': i === currentImageIndex }"
|
||||
@@ -192,7 +192,7 @@ function onTouchEnd(e: TouchEvent) {
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
import { useFeedStore } from '@/stores/feed.store';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useFeed } from '@/composables/useFeed';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import FeedCard from './FeedCard.vue';
|
||||
import EmptyState from '@/components/common/EmptyState.vue';
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
|
||||
|
||||
const feedStore = useFeedStore();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const feedStore = useFeed();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const prefersReducedMotion = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useFeedStore } from '@/stores/feed.store';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { useFeed } from '@/composables/useFeed';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
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 AppDrawer from '@/components/common/AppDrawer.vue';
|
||||
|
||||
defineProps<{ open: boolean }>();
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const feedStore = useFeedStore();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUi();
|
||||
const feedStore = useFeed();
|
||||
const authStore = useAuth();
|
||||
|
||||
const filters = reactive({
|
||||
cityId: '',
|
||||
@@ -103,7 +103,7 @@ function reset() {
|
||||
class="filters__tag"
|
||||
:class="{ 'filters__tag--active': filters.tagIds.includes(tag.id) }"
|
||||
@click="toggleTag(tag.id)"
|
||||
>{{ tag.name }}</button>
|
||||
>{{ tag.value }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useRoute } from 'vue-router';
|
||||
import TauriTitlebar from './TauriTitlebar.vue';
|
||||
import SideNav from './SideNav.vue';
|
||||
import BottomNav from './BottomNav.vue';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const authStore = useAuth();
|
||||
|
||||
const isTauri = typeof window !== 'undefined' && !!window.__TAURI__;
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const navItems = [
|
||||
{ path: '/feed', label: 'Лента', icon: 'grid' },
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { apiClient } from '@/api/client';
|
||||
|
||||
interface MediaItem {
|
||||
@@ -12,7 +12,7 @@ interface MediaItem {
|
||||
const props = defineProps<{ profileId: string; editable?: boolean }>();
|
||||
const emit = defineEmits<{ updated: [] }>();
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const uiStore = useUi();
|
||||
const items = ref<MediaItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const uploading = ref(false);
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
import { reactive, watch, ref } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, helpers } from '@vuelidate/validators';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useProfileStore } from '@/stores/profile.store';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useProfile } from '@/composables/useProfile';
|
||||
import { apiClient } from '@/api/client';
|
||||
import type { UserProfile } from '@/stores/auth.store';
|
||||
import type { District } from '@/stores/ui.store';
|
||||
import type { UserProfile } from '@/composables/useAuth';
|
||||
import type { District } from '@/composables/useUi';
|
||||
import AppInput from '@/components/common/AppInput.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
|
||||
const props = defineProps<{ profile: UserProfile }>();
|
||||
const emit = defineEmits<{ saved: [UserProfile]; cancel: [] }>();
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuthStore();
|
||||
const profileStore = useProfileStore();
|
||||
const uiStore = useUi();
|
||||
const authStore = useAuth();
|
||||
const profileStore = useProfile();
|
||||
|
||||
const form = reactive({
|
||||
name: props.profile.name,
|
||||
@@ -28,7 +28,7 @@ const form = reactive({
|
||||
nation: props.profile.nation ?? '',
|
||||
height: props.profile.height ?? 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[]>([]);
|
||||
@@ -136,7 +136,7 @@ async function save() {
|
||||
class="profile-editor__tag"
|
||||
:class="{ 'profile-editor__tag--active': form.tagIds.includes(tag.id) }"
|
||||
@click="toggleTag(tag.id)"
|
||||
>{{ tag.name }}</button>
|
||||
>{{ tag.value }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { apiClient } from '@/api/client';
|
||||
import AppModal from '@/components/common/AppModal.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
@@ -14,8 +14,8 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const form = reactive({ description: '' });
|
||||
const loading = ref(false);
|
||||
|
||||
130
src/composables/useAuth.ts
Normal file
130
src/composables/useAuth.ts
Normal 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
117
src/composables/useChat.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
66
src/composables/useFeed.ts
Normal file
66
src/composables/useFeed.ts
Normal 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 });
|
||||
}
|
||||
39
src/composables/useProfile.ts
Normal file
39
src/composables/useProfile.ts
Normal 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
82
src/composables/useUi.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { router } from './router';
|
||||
import App from './App.vue';
|
||||
import '@/styles/tailwind.css';
|
||||
import '@/styles/main.scss';
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { _getAccessToken, _setAccessToken } from '@/api/client';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -45,7 +45,7 @@ export const router = createRouter({
|
||||
let _initDone = false;
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
const authStore = useAuth();
|
||||
|
||||
// First navigation: try to restore session from localStorage refresh token
|
||||
if (!_initDone) {
|
||||
@@ -53,12 +53,12 @@ router.beforeEach(async (to, _from, next) => {
|
||||
const storedRefresh = localStorage.getItem('refreshToken');
|
||||
if (storedRefresh && !_getAccessToken()) {
|
||||
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`,
|
||||
{ refreshToken: storedRefresh },
|
||||
);
|
||||
_setAccessToken(res.data.accessToken);
|
||||
localStorage.setItem('refreshToken', res.data.refreshToken);
|
||||
_setAccessToken(res.data.data.accessToken);
|
||||
localStorage.setItem('refreshToken', res.data.data.refreshToken);
|
||||
await authStore.fetchMe();
|
||||
} catch {
|
||||
localStorage.removeItem('refreshToken');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { apiClient } from '@/api/client';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
|
||||
@@ -17,7 +17,7 @@ interface Report {
|
||||
reporterName?: string;
|
||||
}
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const uiStore = useUi();
|
||||
const reports = ref<Report[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ import { reactive, ref } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, helpers } from '@vuelidate/validators';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import AppInput from '@/components/common/AppInput.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const form = reactive({ phone: '', password: '' });
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -3,14 +3,14 @@ import { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, helpers } from '@vuelidate/validators';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import AppInput from '@/components/common/AppInput.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const form = reactive({ phone: '', password: '', confirmPassword: '' });
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useChatStore } from '@/stores/chat.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useChat } from '@/composables/useChat';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
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 ChatInput from '@/components/chat/ChatInput.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';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const chatStore = useChatStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const chatStore = useChat();
|
||||
const uiStore = useUi();
|
||||
|
||||
const chatId = route.params.chatId as string;
|
||||
const profileId = computed(() => authStore.activeProfile?.id ?? '');
|
||||
@@ -23,7 +23,7 @@ const messagesEnd = ref<HTMLElement | null>(null);
|
||||
const confirmClose = ref(false);
|
||||
|
||||
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
|
||||
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() {
|
||||
if (!profileId.value) return;
|
||||
try {
|
||||
await chatStore.closeChat(chatId, profileId.value);
|
||||
confirmClose.value = false;
|
||||
history.back();
|
||||
goBack();
|
||||
} catch {
|
||||
uiStore.addToast('Не удалось закрыть чат', 'error');
|
||||
}
|
||||
@@ -82,20 +84,20 @@ async function doCloseChat() {
|
||||
<div 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">
|
||||
<path d="M19 12H5M12 5l-7 7 7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="chat-room__partner">
|
||||
<img
|
||||
v-if="chat?.partner.avatarUrl"
|
||||
v-if="chat?.partner?.avatarUrl"
|
||||
:src="chat.partner.avatarUrl"
|
||||
class="chat-room__avatar"
|
||||
:alt="chat.partner.name"
|
||||
:alt="chat.partner?.name"
|
||||
/>
|
||||
<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>
|
||||
<button
|
||||
class="chat-room__close-btn"
|
||||
@@ -135,7 +137,7 @@ async function doCloseChat() {
|
||||
v-for="msg in group.messages"
|
||||
:key="msg.id"
|
||||
:message="msg"
|
||||
:is-mine="msg.senderId === authStore.activeProfile?.id"
|
||||
:is-mine="msg.profileId === authStore.activeProfile?.id"
|
||||
/>
|
||||
</div>
|
||||
<div ref="messagesEnd" />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useChatStore } from '@/stores/chat.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useChat } from '@/composables/useChat';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
|
||||
import EmptyState from '@/components/common/EmptyState.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const chatStore = useChatStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const chatStore = useChat();
|
||||
const uiStore = useUi();
|
||||
const loading = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -55,13 +55,13 @@ function formatTime(dateStr: string) {
|
||||
<RouterLink
|
||||
:to="`/chats/${chat.id}`"
|
||||
class="chat-item"
|
||||
:class="{ 'chat-item--inactive': !chat.isActive }"
|
||||
:class="{ 'chat-item--inactive': chat.status === 'closed' }"
|
||||
>
|
||||
<div class="chat-item__avatar-wrap">
|
||||
<img
|
||||
v-if="chat.partner.avatarUrl"
|
||||
v-if="chat.partner?.avatarUrl"
|
||||
:src="chat.partner.avatarUrl"
|
||||
:alt="chat.partner.name"
|
||||
:alt="chat.partner?.name"
|
||||
class="chat-item__avatar"
|
||||
/>
|
||||
<div v-else class="chat-item__avatar chat-item__avatar--placeholder">
|
||||
@@ -70,7 +70,7 @@ function formatTime(dateStr: string) {
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 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">
|
||||
<rect x="3" y="7" width="10" height="8" rx="1"/>
|
||||
<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__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">
|
||||
{{ formatTime(chat.lastMessage.createdAt) }}
|
||||
</span>
|
||||
@@ -91,7 +91,7 @@ function formatTime(dateStr: string) {
|
||||
<p v-else class="chat-item__preview chat-item__preview--empty">Начните переписку</p>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { apiClient } from '@/api/client';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
import AppModal from '@/components/common/AppModal.vue';
|
||||
@@ -22,8 +22,8 @@ interface DateItem {
|
||||
isIncoming: boolean;
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const dates = ref<DateItem[]>([]);
|
||||
const statuses = ref<DateStatus[]>([]);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useFeedStore } from '@/stores/feed.store';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useFeed } from '@/composables/useFeed';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import FeedCardStack from '@/components/feed/FeedCardStack.vue';
|
||||
import FeedFilters from '@/components/feed/FeedFilters.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
|
||||
const feedStore = useFeedStore();
|
||||
const authStore = useAuthStore();
|
||||
const feedStore = useFeed();
|
||||
const authStore = useAuth();
|
||||
|
||||
const filtersOpen = ref(false);
|
||||
const viewMode = ref<'stack' | 'scroll'>('stack');
|
||||
@@ -83,8 +83,8 @@ onMounted(() => {
|
||||
>
|
||||
<RouterLink :to="`/profile/${profile.id}`" class="feed-grid__link">
|
||||
<img
|
||||
v-if="profile.avatarUrl"
|
||||
:src="profile.avatarUrl"
|
||||
v-if="profile.media?.[0]?.path"
|
||||
:src="profile.media[0].path"
|
||||
:alt="profile.name"
|
||||
class="feed-grid__img"
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useChatStore } from '@/stores/chat.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useChat } from '@/composables/useChat';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { useRouter } from 'vue-router';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
@@ -23,9 +23,9 @@ interface Match {
|
||||
hasChat: boolean;
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const chatStore = useChatStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const chatStore = useChat();
|
||||
const uiStore = useUi();
|
||||
const router = useRouter();
|
||||
|
||||
const matches = ref<Match[]>([]);
|
||||
|
||||
@@ -3,35 +3,35 @@ import { reactive, ref, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, helpers } from '@vuelidate/validators';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { apiClient } from '@/api/client';
|
||||
import type { CreateProfileDto } from '@/api/api';
|
||||
import type { UserProfile } from '@/stores/auth.store';
|
||||
import type { District } from '@/stores/ui.store';
|
||||
import type { UserProfile } from '@/composables/useAuth';
|
||||
import type { District } from '@/composables/useUi';
|
||||
import AppInput from '@/components/common/AppInput.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const step = ref(1);
|
||||
const totalSteps = 4;
|
||||
const loading = ref(false);
|
||||
|
||||
const form = reactive<CreateProfileDto & { confirmStep?: number }>({
|
||||
const form = reactive({
|
||||
name: '',
|
||||
birthDate: '',
|
||||
gender: 'female',
|
||||
gender: 'female' as 'female' | 'male',
|
||||
cityId: '',
|
||||
districtId: '',
|
||||
description: '',
|
||||
nation: '',
|
||||
height: undefined,
|
||||
weight: undefined,
|
||||
tagIds: [],
|
||||
height: undefined as number | undefined | null,
|
||||
weight: undefined as number | undefined | null,
|
||||
tagIds: [] as string[],
|
||||
});
|
||||
|
||||
const selectedTags = ref<string[]>([]);
|
||||
@@ -85,7 +85,7 @@ async function finish() {
|
||||
loading.value = true;
|
||||
try {
|
||||
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);
|
||||
uiStore.addToast('Профиль создан', 'success');
|
||||
router.replace('/feed');
|
||||
@@ -207,7 +207,7 @@ function skip() {
|
||||
:class="{ 'setup__tag--active': selectedTags.includes(tag.id) }"
|
||||
@click="toggleTag(tag.id)"
|
||||
>
|
||||
{{ tag.name }}
|
||||
{{ tag.value }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="uiStore.tags.length === 0" class="setup__hint setup__hint--muted">Теги загружаются...</p>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
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 MediaGallery from '@/components/profile/MediaGallery.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
import AppModal from '@/components/common/AppModal.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
|
||||
const editing = ref(false);
|
||||
const confirmDelete = ref(false);
|
||||
@@ -41,12 +41,12 @@ function logout() {
|
||||
authStore.logout();
|
||||
}
|
||||
|
||||
function cityName(cityId?: string) {
|
||||
function cityName(cityId?: string | null) {
|
||||
return uiStore.cities.find((c) => c.id === cityId)?.name ?? '';
|
||||
}
|
||||
|
||||
function tagNames(tagIds?: string[]) {
|
||||
return (tagIds ?? []).map((id) => uiStore.tags.find((t) => t.id === id)?.name ?? id);
|
||||
function tagValues(tags?: Array<{ id: string; value: string }>) {
|
||||
return (tags ?? []).map((t) => t.value);
|
||||
}
|
||||
|
||||
function calcAge(birthDate: string) {
|
||||
@@ -66,8 +66,8 @@ function calcAge(birthDate: string) {
|
||||
<div class="my-profile__hero">
|
||||
<div class="my-profile__avatar-wrap">
|
||||
<img
|
||||
v-if="profile.avatarUrl"
|
||||
:src="profile.avatarUrl"
|
||||
v-if="profile.media?.[0]?.path"
|
||||
:src="profile.media[0].path"
|
||||
:alt="profile.name"
|
||||
class="my-profile__avatar"
|
||||
/>
|
||||
@@ -113,10 +113,10 @@ function calcAge(birthDate: string) {
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useUiStore } from '@/stores/ui.store';
|
||||
import { useChatStore } from '@/stores/chat.store';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useUi } from '@/composables/useUi';
|
||||
import { useChat } from '@/composables/useChat';
|
||||
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 AppModal from '@/components/common/AppModal.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';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const chatStore = useChatStore();
|
||||
const authStore = useAuth();
|
||||
const uiStore = useUi();
|
||||
const chatStore = useChat();
|
||||
|
||||
const profileId = route.params.profileId as string;
|
||||
const profile = ref<UserProfile | null>(null);
|
||||
@@ -44,13 +44,18 @@ const age = computed(() => {
|
||||
return a;
|
||||
});
|
||||
|
||||
const cityName = computed(() => uiStore.cities.find((c) => c.id === profile.value?.cityId)?.name ?? '');
|
||||
const tagNames = computed(() => (profile.value?.tagIds ?? []).map((id) => uiStore.tags.find((t) => t.id === id)?.name ?? id));
|
||||
const cityName = computed(() => {
|
||||
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 isOwnProfile = computed(() =>
|
||||
authStore.profiles.some((p) => p.id === profileId),
|
||||
);
|
||||
|
||||
function goBack() { window.history.back(); }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,8 +68,8 @@ const isOwnProfile = computed(() =>
|
||||
<!-- Image strip -->
|
||||
<div class="profile-detail__cover">
|
||||
<img
|
||||
v-if="profile.mediaUrls?.[currentImageIndex]"
|
||||
:src="profile.mediaUrls[currentImageIndex]"
|
||||
v-if="profile.media?.[currentImageIndex]?.path"
|
||||
:src="profile.media[currentImageIndex].path"
|
||||
:alt="profile.name"
|
||||
class="profile-detail__cover-img"
|
||||
/>
|
||||
@@ -78,14 +83,14 @@ const isOwnProfile = computed(() =>
|
||||
aria-label="Предыдущее фото"
|
||||
>‹</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"
|
||||
@click="currentImageIndex++"
|
||||
aria-label="Следующее фото"
|
||||
>›</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">
|
||||
<path d="M19 12H5M12 5l-7 7 7 7"/>
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user