refactor(composables): migrate stores →

composables, align with updated API

  - Replace deleted Pinia stores with
  module-level singleton composables
    (useAuth, useChat, useFeed, useUi) — all
  return reactive({...}) so
    Refs auto-unwrap in both templates and
  script code

  - Align entire codebase with new
  swagger-generated api.ts types:
    · TagDto.value  (was .name) — FeedCard,
  FeedFilters, ProfileEditor,
      ProfileSetupView, MyProfileView,
  ProfileDetailView, useUi
    · MediaItemDto[] / .path  (was mediaUrls[],
  avatarUrl) — FeedCard,
      FeedView, MyProfileView,
  ProfileDetailView
    · ChatDto.status 'active'|'closed'  (was
  isActive: boolean) —
      ChatRoomView, ChatsListView
    · MessageDto.profileId  (was senderId) —
  ChatRoomView, ChatBubble
    · MeResponseDto → fetchMe now calls /me +
  /profiles/my in parallel
    · Token refresh: res.data.data.accessToken
  (nested wrapper) —
      router/index.ts aligned with client.ts
  interceptor

  - Fix FeedCard, ChatBubble imports pointing
  to deleted store files
  - Fix ProfileSetupView form type to avoid
  string|undefined on v-model
  - Fix history.back() → window.history.back()
  via goBack() helper
  - Fix chat.unreadCount possibly-undefined
  guard in ChatsListView
  - Fix MapPicker Leaflet icon cast (as unknown
  as Record<string, unknown>)
This commit is contained in:
Oscar
2026-06-08 15:01:54 +03:00
parent f5e34f3a97
commit 10d696f4ca
41 changed files with 913 additions and 673 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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[]>([]);

View File

@@ -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"
/>

View File

@@ -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[]>([]);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>