✨ feat(src/composables/useFeed.ts): добавляет поддержку бесконечной прокрутки для ленты новостей
✨ feat(package.json): добавляет скрипт для генерации спрайта и обновляет зависимости 🔧 refactor(src/composables/useChat.ts): упрощает логику управления чатом, убирает устаревшие функции 🔧 refactor(src/views/chat/ChatRoomView.vue): обновляет логику загрузки сообщений с использованием новых хуков 🔧 refactor(src/views/matches/MatchesView.vue): улучшает обработку совпадений с использованием новых хуков ✨ feat(src/App.vue): добавляет загрузку справочных данных с использованием Vue Query ✨ feat(src/main.ts): интегрирует Vue Query в приложение для управления состоянием запросов ✨ feat(src/views/profile/MyProfileView.vue): добавляет мутацию для удаления профиля с использованием Vue Query 🔧 refactor(src/components/layout/BottomNav.vue): заменяет компонент иконки на более универсальный компонент AppIcon
This commit is contained in:
@@ -70,12 +70,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { apiClient } from '@/api/client'
|
||||
import { computed } from 'vue'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import { useUi } from '@/composables/useUi'
|
||||
import { useReportsQuery } from '@/queries/useReportsQuery'
|
||||
|
||||
interface Report {
|
||||
id: string
|
||||
@@ -89,26 +89,14 @@ interface Report {
|
||||
}
|
||||
|
||||
const uiStore = useUi()
|
||||
const reports = ref<Report[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await apiClient.api.reportsControllerGetAll() as unknown as Report[]
|
||||
reports.value = res
|
||||
}
|
||||
catch {
|
||||
uiStore.addToast('Не удалось загрузить жалобы', 'error')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
const { query: { data: reportsData, isLoading: loading }, banUser: banUserMutation } = useReportsQuery()
|
||||
|
||||
const reports = computed(() => (reportsData.value as unknown as Report[]) ?? [])
|
||||
|
||||
async function banUser(userId: string) {
|
||||
try {
|
||||
await apiClient.api.usersControllerBan(userId)
|
||||
await banUserMutation.mutateAsync(userId)
|
||||
uiStore.addToast('Пользователь заблокирован', 'success')
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div v-if="chatStore.loading" class="chat-room__loading">
|
||||
<div v-if="messagesQuery.isLoading.value" class="chat-room__loading">
|
||||
<LoadingSpinner size="md" />
|
||||
</div>
|
||||
|
||||
<div v-else class="chat-room__messages" :class="{ 'chat-room__messages--blurred': isLocked }">
|
||||
<div v-else-if="!messagesQuery.isLoading.value" class="chat-room__messages" :class="{ 'chat-room__messages--blurred': isLocked }">
|
||||
<div
|
||||
v-for="group in groupedMessages"
|
||||
:key="group.date"
|
||||
@@ -87,8 +87,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ChatMessage } from '@/composables/useChat'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import type { MessageDto } from '@/api/api'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ChatBubble from '@/components/chat/ChatBubble.vue'
|
||||
import ChatInput from '@/components/chat/ChatInput.vue'
|
||||
@@ -96,27 +96,29 @@ import AppButton from '@/components/common/AppButton.vue'
|
||||
import AppModal from '@/components/common/AppModal.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useChat } from '@/composables/useChat'
|
||||
import { useUi } from '@/composables/useUi'
|
||||
import { useChatsQuery } from '@/queries/useChatsQuery'
|
||||
import { useMessagesQuery } from '@/queries/useMessagesQuery'
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuth()
|
||||
const chatStore = useChat()
|
||||
const uiStore = useUi()
|
||||
|
||||
const chatId = route.params.chatId as string
|
||||
const profileId = computed(() => authStore.activeProfile?.id ?? '')
|
||||
const profileId = computed(() => authStore.activeProfile?.id)
|
||||
const messagesEnd = ref<HTMLElement | null>(null)
|
||||
const confirmClose = ref(false)
|
||||
|
||||
const chat = computed(() => chatStore.chats.find(c => c.id === chatId))
|
||||
const { query: chatsQuery, closeChat: closeChatMutation } = useChatsQuery(profileId)
|
||||
const { query: messagesQuery, sendMessage: sendMessageMutation } = useMessagesQuery(chatId, profileId)
|
||||
|
||||
const chat = computed(() => chatsQuery.data.value?.find(c => c.id === chatId))
|
||||
const isLocked = computed(() => chat.value?.status === 'closed')
|
||||
|
||||
// Group messages by date
|
||||
const groupedMessages = computed(() => {
|
||||
const groups: Array<{ date: string, messages: ChatMessage[] }> = []
|
||||
const groups: Array<{ date: string, messages: MessageDto[] }> = []
|
||||
let lastDate = ''
|
||||
for (const msg of chatStore.messages) {
|
||||
for (const msg of (messagesQuery.data.value ?? [])) {
|
||||
const d = new Date(msg.createdAt).toLocaleDateString('ru', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
if (d !== lastDate) {
|
||||
groups.push({ date: d, messages: [] })
|
||||
@@ -132,23 +134,11 @@ async function scrollToBottom() {
|
||||
messagesEnd.value?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!profileId.value)
|
||||
return
|
||||
await chatStore.fetchMessages(chatId, profileId.value)
|
||||
await scrollToBottom()
|
||||
chatStore.startPolling(chatId, profileId.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
chatStore.stopPolling()
|
||||
})
|
||||
watch(() => messagesQuery.data.value?.length, scrollToBottom)
|
||||
|
||||
async function send(text: string, mediaUrl?: string, mediaType?: 'photo' | 'voice' | 'video') {
|
||||
if (!profileId.value)
|
||||
return
|
||||
try {
|
||||
await chatStore.sendMessage(chatId, profileId.value, { text, mediaUrl, mediaType })
|
||||
await sendMessageMutation.mutateAsync({ text, mediaUrl, mediaType })
|
||||
await scrollToBottom()
|
||||
}
|
||||
catch {
|
||||
@@ -161,10 +151,8 @@ function goBack() {
|
||||
}
|
||||
|
||||
async function doCloseChat() {
|
||||
if (!profileId.value)
|
||||
return
|
||||
try {
|
||||
await chatStore.closeChat(chatId, profileId.value)
|
||||
await closeChatMutation.mutateAsync({ chatId })
|
||||
confirmClose.value = false
|
||||
goBack()
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-else-if="chatStore.chats.length === 0"
|
||||
v-else-if="(chats ?? []).length === 0"
|
||||
title="Нет активных чатов"
|
||||
description="Найдите совпадения и начните общение"
|
||||
icon="chat"
|
||||
/>
|
||||
|
||||
<ul v-else class="chats-list__list" role="list">
|
||||
<li v-for="chat in chatStore.chats" :key="chat.id">
|
||||
<li v-for="chat in (chats ?? [])" :key="chat.id">
|
||||
<RouterLink
|
||||
:to="`/chats/${chat.id}`"
|
||||
class="chat-item"
|
||||
@@ -70,33 +70,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useChat } from '@/composables/useChat'
|
||||
import { useUi } from '@/composables/useUi'
|
||||
import { useChatsQuery } from '@/queries/useChatsQuery'
|
||||
|
||||
const authStore = useAuth()
|
||||
const chatStore = useChat()
|
||||
const uiStore = useUi()
|
||||
const loading = ref(false)
|
||||
const profileId = computed(() => authStore.activeProfile?.id)
|
||||
|
||||
onMounted(async () => {
|
||||
const profileId = authStore.activeProfile?.id
|
||||
if (!profileId)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
await chatStore.fetchChats(profileId)
|
||||
}
|
||||
catch {
|
||||
uiStore.addToast('Не удалось загрузить чаты', 'error')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
const { query: { data: chats, isLoading: loading } } = useChatsQuery(profileId)
|
||||
|
||||
function formatTime(dateStr: string) {
|
||||
const d = new Date(dateStr)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-else-if="dates.length === 0"
|
||||
v-else-if="dates.length === 0 && !loading"
|
||||
title="Нет предстоящих встреч"
|
||||
description="Предложите встречу из профиля совпадения"
|
||||
icon="calendar"
|
||||
@@ -77,13 +77,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { apiClient } from '@/api/client'
|
||||
import { computed, ref } from 'vue'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUi } from '@/composables/useUi'
|
||||
import { useDatesQuery } from '@/queries/useDatesQuery'
|
||||
|
||||
interface DateStatus { id: string, name: string }
|
||||
interface DateItem {
|
||||
@@ -102,31 +102,17 @@ interface DateItem {
|
||||
const authStore = useAuth()
|
||||
const uiStore = useUi()
|
||||
|
||||
const dates = ref<DateItem[]>([])
|
||||
const statuses = ref<DateStatus[]>([])
|
||||
const loading = ref(false)
|
||||
const profileId = computed(() => authStore.activeProfile?.id)
|
||||
const actionLoading = ref<string | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
const profileId = authStore.activeProfile?.id
|
||||
if (!profileId)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
const [datesRes, statusesRes] = await Promise.all([
|
||||
apiClient.api.datesControllerGetDates({ profileId }) as unknown as DateItem[],
|
||||
apiClient.api.datesControllerGetStatuses() as unknown as DateStatus[],
|
||||
])
|
||||
dates.value = datesRes
|
||||
statuses.value = statusesRes
|
||||
}
|
||||
catch {
|
||||
uiStore.addToast('Не удалось загрузить встречи', 'error')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
const {
|
||||
datesQuery: { data: datesData, isLoading: loading },
|
||||
statusesQuery: { data: statusesData },
|
||||
updateStatus: updateStatusMutation,
|
||||
} = useDatesQuery(profileId)
|
||||
|
||||
const dates = computed(() => (datesData.value as unknown as DateItem[]) ?? [])
|
||||
const statuses = computed(() => (statusesData.value as unknown as DateStatus[]) ?? [])
|
||||
|
||||
function statusLabel(statusId: string) {
|
||||
return statuses.value.find(s => s.id === statusId)?.name ?? statusId
|
||||
@@ -146,15 +132,9 @@ function statusColor(statusId: string) {
|
||||
}
|
||||
|
||||
async function updateStatus(dateId: string, statusId: string) {
|
||||
const profileId = authStore.activeProfile?.id
|
||||
if (!profileId)
|
||||
return
|
||||
actionLoading.value = `${dateId}-${statusId}`
|
||||
try {
|
||||
await apiClient.api.datesControllerUpdateStatus({ id: dateId, profileId }, { statusId })
|
||||
const idx = dates.value.findIndex(d => d.id === dateId)
|
||||
if (idx !== -1)
|
||||
dates.value[idx].statusId = statusId
|
||||
await updateStatusMutation.mutateAsync({ dateId, statusId })
|
||||
uiStore.addToast('Статус обновлён', 'success')
|
||||
}
|
||||
catch {
|
||||
@@ -175,7 +155,6 @@ function formatDateTime(iso: string) {
|
||||
})
|
||||
}
|
||||
|
||||
// Pending = first two status ids roughly; accept/decline/complete = 3+
|
||||
const _pendingStatusId = computed(() => statuses.value[0]?.id ?? '')
|
||||
const acceptedStatusId = computed(() => statuses.value[1]?.id ?? '')
|
||||
const declinedStatusId = computed(() => statuses.value[2]?.id ?? '')
|
||||
|
||||
@@ -86,25 +86,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import FeedCardStack from '@/components/feed/FeedCardStack.vue'
|
||||
import FeedFilters from '@/components/feed/FeedFilters.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useFeed } from '@/composables/useFeed'
|
||||
|
||||
const feedStore = useFeed()
|
||||
const authStore = useAuth()
|
||||
|
||||
const filtersOpen = ref(false)
|
||||
const viewMode = ref<'stack' | 'scroll'>('stack')
|
||||
|
||||
onMounted(() => {
|
||||
const profileId = authStore.activeProfile?.id
|
||||
if (profileId && feedStore.cards.length === 0) {
|
||||
feedStore.fetchNextPage(profileId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h1 class="matches-view__title">
|
||||
Совпадения
|
||||
</h1>
|
||||
<span class="meta">{{ matches.length }} {{ matches.length === 1 ? 'человек' : 'людей' }}</span>
|
||||
<span class="meta">{{ (matches ?? []).length }} {{ (matches ?? []).length === 1 ? 'человек' : 'людей' }}</span>
|
||||
</header>
|
||||
|
||||
<div v-if="loading" class="matches-view__loading">
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-else-if="matches.length === 0"
|
||||
v-else-if="(matches ?? []).length === 0"
|
||||
title="Пока нет совпадений"
|
||||
description="Ставьте лайки, чтобы находить тех, кто ответит взаимностью"
|
||||
icon="heart"
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<div v-else class="matches-list">
|
||||
<article
|
||||
v-for="match in matches"
|
||||
v-for="match in (matches ?? [])"
|
||||
:key="match.id"
|
||||
class="match-card"
|
||||
>
|
||||
@@ -61,26 +61,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { MatchDto, ProfileResponseDto } from '@/api/api'
|
||||
import { apiClient } from '@/api/client'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useChat } from '@/composables/useChat'
|
||||
import { useUi } from '@/composables/useUi'
|
||||
|
||||
type EnrichedMatch = MatchDto & { partnerProfile: ProfileResponseDto }
|
||||
import { useChatsQuery } from '@/queries/useChatsQuery'
|
||||
import { useMatchesQuery } from '@/queries/useMatchesQuery'
|
||||
import type { EnrichedMatch } from '@/queries/useMatchesQuery'
|
||||
|
||||
const authStore = useAuth()
|
||||
const chatStore = useChat()
|
||||
const uiStore = useUi()
|
||||
const router = useRouter()
|
||||
|
||||
const matches = ref<EnrichedMatch[]>([])
|
||||
const loading = ref(false)
|
||||
const profileId = computed(() => authStore.activeProfile?.id)
|
||||
|
||||
const { data: matches, isLoading: loading } = useMatchesQuery(profileId)
|
||||
const { openChat: openChatMutation } = useChatsQuery(profileId)
|
||||
|
||||
function calcAge(birthDate: string): number | undefined {
|
||||
const birth = new Date(birthDate)
|
||||
@@ -93,42 +92,14 @@ function calcAge(birthDate: string): number | undefined {
|
||||
return age
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const profileId = authStore.activeProfile?.id
|
||||
if (!profileId)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
const matchDtos = await apiClient.api.likesControllerGetMyMatches({ profileId })
|
||||
matches.value = await Promise.all(
|
||||
matchDtos.map(async (m) => {
|
||||
const partnerProfileId = m.profile1Id === profileId ? m.profile2Id : m.profile1Id
|
||||
const partnerProfile = await apiClient.api.profilesControllerFindOne(partnerProfileId)
|
||||
return { ...m, partnerProfile }
|
||||
}),
|
||||
)
|
||||
}
|
||||
catch {
|
||||
uiStore.addToast('Не удалось загрузить совпадения', 'error')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function openChat(match: EnrichedMatch) {
|
||||
const profileId = authStore.activeProfile?.id
|
||||
if (!profileId)
|
||||
return
|
||||
try {
|
||||
const chat = await chatStore.openChat(profileId, match.id)
|
||||
const chat = await openChatMutation.mutateAsync({ matchId: match.id })
|
||||
router.push(`/chats/${chat.id}`)
|
||||
}
|
||||
catch (err: unknown) {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })
|
||||
?.response
|
||||
?.data
|
||||
?.message ?? 'Не удалось открыть чат'
|
||||
?.response?.data?.message ?? 'Не удалось открыть чат'
|
||||
uiStore.addToast(msg, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,22 +137,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { UserProfile } from '@/composables/useAuth'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiClient } from '@/api/client'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import AppModal from '@/components/common/AppModal.vue'
|
||||
import MediaGallery from '@/components/profile/MediaGallery.vue'
|
||||
import ProfileEditor from '@/components/profile/ProfileEditor.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUi } from '@/composables/useUi'
|
||||
import { useDeleteProfileMutation } from '@/queries/useProfileQuery'
|
||||
|
||||
const authStore = useAuth()
|
||||
const uiStore = useUi()
|
||||
const deleteProfileMutation = useDeleteProfileMutation()
|
||||
|
||||
const editing = ref(false)
|
||||
const confirmDelete = ref(false)
|
||||
const deleting = ref(false)
|
||||
|
||||
const profile = computed(() => authStore.activeProfile)
|
||||
const deleting = computed(() => deleteProfileMutation.isPending.value)
|
||||
|
||||
function onSaved(_updated: UserProfile) {
|
||||
editing.value = false
|
||||
@@ -161,9 +162,8 @@ function onSaved(_updated: UserProfile) {
|
||||
async function doDelete() {
|
||||
if (!profile.value)
|
||||
return
|
||||
deleting.value = true
|
||||
try {
|
||||
await apiClient.api.profilesControllerDelete(profile.value.id)
|
||||
await deleteProfileMutation.mutateAsync(profile.value.id)
|
||||
authStore.removeProfile(profile.value.id)
|
||||
confirmDelete.value = false
|
||||
uiStore.addToast('Профиль удалён', 'success')
|
||||
@@ -171,9 +171,6 @@ async function doDelete() {
|
||||
catch {
|
||||
uiStore.addToast('Не удалось удалить профиль', 'error')
|
||||
}
|
||||
finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
|
||||
@@ -113,43 +113,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UserProfile } from '@/composables/useAuth'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { apiClient } from '@/api/client'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import AppModal from '@/components/common/AppModal.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import DateProposalForm from '@/components/dates/DateProposalForm.vue'
|
||||
import ReportModal from '@/components/reports/ReportModal.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useChat } from '@/composables/useChat'
|
||||
import { useUi } from '@/composables/useUi'
|
||||
import { useProfileQuery } from '@/queries/useProfileQuery'
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuth()
|
||||
const uiStore = useUi()
|
||||
const _chatStore = useChat()
|
||||
|
||||
const profileId = route.params.profileId as string
|
||||
const profile = ref<UserProfile | null>(null)
|
||||
const loading = ref(false)
|
||||
const reportOpen = ref(false)
|
||||
const dateOpen = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await apiClient.api.profilesControllerFindOne(profileId)
|
||||
profile.value = res
|
||||
}
|
||||
catch {
|
||||
uiStore.addToast('Не удалось загрузить профиль', 'error')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
const { data: profile, isLoading: loading } = useProfileQuery(profileId)
|
||||
|
||||
const age = computed(() => {
|
||||
if (!profile.value?.birthDate)
|
||||
|
||||
Reference in New Issue
Block a user