Files
dating-app-frontend/src/views/matches/MatchesView.vue
Oscar 78fe3b5a98 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
2026-06-09 10:29:52 +03:00

207 lines
5.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="matches-view">
<header class="matches-view__header">
<h1 class="matches-view__title">
Совпадения
</h1>
<span class="meta">{{ (matches ?? []).length }} {{ (matches ?? []).length === 1 ? 'человек' : 'людей' }}</span>
</header>
<div v-if="loading" class="matches-view__loading">
<LoadingSpinner size="lg" />
</div>
<EmptyState
v-else-if="(matches ?? []).length === 0"
title="Пока нет совпадений"
description="Ставьте лайки, чтобы находить тех, кто ответит взаимностью"
icon="heart"
/>
<div v-else class="matches-list">
<article
v-for="match in (matches ?? [])"
:key="match.id"
class="match-card"
>
<RouterLink :to="`/profile/${match.partnerProfile.id}`" class="match-card__avatar-wrap">
<img
v-if="match.partnerProfile.media[0]?.path"
:src="match.partnerProfile.media[0].path"
:alt="match.partnerProfile.name"
class="match-card__avatar"
>
<div v-else class="match-card__avatar match-card__avatar--placeholder">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" width="28" height="28" opacity="0.3">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" />
</svg>
</div>
</RouterLink>
<div class="match-card__info">
<RouterLink :to="`/profile/${match.partnerProfile.id}`" class="match-card__name">
{{ match.partnerProfile.name }}
<span v-if="match.partnerProfile.birthDate" class="match-card__age">, {{ calcAge(match.partnerProfile.birthDate) }}</span>
</RouterLink>
<span v-if="match.partnerProfile.city?.name" class="meta match-card__city">
{{ match.partnerProfile.city.name }}
</span>
</div>
<AppButton
variant="primary"
size="sm"
@click="openChat(match)"
>
Написать
</AppButton>
</article>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
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 { useChatsQuery } from '@/queries/useChatsQuery'
import { useMatchesQuery } from '@/queries/useMatchesQuery'
import type { EnrichedMatch } from '@/queries/useMatchesQuery'
const authStore = useAuth()
const uiStore = useUi()
const router = useRouter()
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)
if (Number.isNaN(birth.getTime()))
return undefined
const today = new Date()
let age = today.getFullYear() - birth.getFullYear()
if (today.getMonth() < birth.getMonth() || (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate()))
age--
return age
}
async function openChat(match: EnrichedMatch) {
try {
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 ?? 'Не удалось открыть чат'
uiStore.addToast(msg, 'error')
}
}
</script>
<style scoped lang="scss">
.matches-view {
height: 100%;
display: flex;
flex-direction: column;
&__header {
display: flex;
align-items: baseline;
gap: 12px;
padding: 20px 24px 16px;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
&__title {
font-family: var(--font-display);
font-size: 1.5rem;
color: var(--color-cream);
margin: 0;
}
&__loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
.matches-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.match-card {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 24px;
transition: background var(--transition-fast);
&:hover {
background: rgba(240, 235, 224, 0.03);
}
&__avatar-wrap {
flex-shrink: 0;
text-decoration: none;
}
&__avatar {
width: 52px;
height: 52px;
border-radius: 50%;
object-fit: cover;
&--placeholder {
background: var(--color-surface-2);
display: flex;
align-items: center;
justify-content: center;
}
}
&__info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
&__name {
font-family: var(--font-mono);
font-size: 0.9375rem;
font-weight: 500;
color: var(--color-cream);
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: var(--color-signal);
}
}
&__age {
font-weight: 400;
opacity: 0.6;
}
&__city {
color: var(--color-muted);
}
}
</style>