Files
dating-app-frontend/src/views/profile/ProfileDetailView.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

331 lines
8.1 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="profile-detail">
<div v-if="loading" class="profile-detail__loading">
<LoadingSpinner size="lg" />
</div>
<template v-else-if="profile">
<!-- Image strip -->
<div class="profile-detail__cover">
<img
v-if="profile.media?.[currentImageIndex]?.path"
:src="profile.media[currentImageIndex].path"
:alt="profile.name"
class="profile-detail__cover-img"
>
<div v-else class="profile-detail__cover-placeholder" />
<!-- Navigation -->
<button
v-if="currentImageIndex > 0"
class="profile-detail__img-nav profile-detail__img-nav--prev"
aria-label="Предыдущее фото"
@click="currentImageIndex--"
>
</button>
<button
v-if="currentImageIndex < (profile.media?.length ?? 1) - 1"
class="profile-detail__img-nav profile-detail__img-nav--next"
aria-label="Следующее фото"
@click="currentImageIndex++"
>
</button>
<!-- Back button -->
<button class="profile-detail__back" aria-label="Назад" @click="goBack()">
<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>
</button>
</div>
<!-- Content -->
<div class="profile-detail__content">
<div class="profile-detail__header">
<div>
<h1 class="profile-detail__name">
{{ profile.name }}
<span v-if="age" class="profile-detail__age">, {{ age }}</span>
</h1>
<span v-if="cityName" class="meta profile-detail__location">{{ cityName }}</span>
</div>
<!-- Actions (non-own) -->
<div v-if="!isOwnProfile" class="profile-detail__actions">
<AppButton size="sm" variant="primary" @click="dateOpen = true">
Встреча
</AppButton>
<AppButton size="sm" variant="ghost" @click="reportOpen = true">
Пожаловаться
</AppButton>
</div>
</div>
<!-- Description -->
<p v-if="profile.description" class="profile-detail__bio">
{{ profile.description }}
</p>
<!-- Stats -->
<div class="profile-detail__stats">
<div v-if="profile.nation" class="profile-detail__stat">
<span class="meta">Национальность</span>
<span>{{ profile.nation }}</span>
</div>
<div v-if="profile.height" class="profile-detail__stat">
<span class="meta">Рост</span>
<span>{{ profile.height }} см</span>
</div>
<div v-if="profile.weight" class="profile-detail__stat">
<span class="meta">Вес</span>
<span>{{ profile.weight }} кг</span>
</div>
</div>
<!-- Tags -->
<div v-if="tagNames.length" class="profile-detail__tags">
<span v-for="name in tagNames" :key="name" class="profile-detail__tag">{{ name }}</span>
</div>
</div>
</template>
<!-- Report modal -->
<ReportModal
v-if="profile"
:open="reportOpen"
entity-type="profile"
:entity-id="profileId"
@close="reportOpen = false"
/>
<!-- Date proposal -->
<AppModal :open="dateOpen" title="Предложить встречу" size="md" @close="dateOpen = false">
<DateProposalForm
v-if="profile"
:partner-profile-id="profile.id"
@close="dateOpen = false"
@created="dateOpen = false"
/>
</AppModal>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
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 { useUi } from '@/composables/useUi'
import { useProfileQuery } from '@/queries/useProfileQuery'
const route = useRoute()
const authStore = useAuth()
const uiStore = useUi()
const profileId = route.params.profileId as string
const reportOpen = ref(false)
const dateOpen = ref(false)
const { data: profile, isLoading: loading } = useProfileQuery(profileId)
const age = computed(() => {
if (!profile.value?.birthDate)
return null
const b = new Date(profile.value.birthDate)
const now = new Date()
let a = now.getFullYear() - b.getFullYear()
if (now.getMonth() < b.getMonth() || (now.getMonth() === b.getMonth() && now.getDate() < b.getDate()))
a--
return a
})
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>
<style scoped lang="scss">
.profile-detail {
height: 100%;
overflow-y: auto;
&__loading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
&__cover {
position: relative;
height: 60dvh;
background: var(--color-surface-2);
@include mobile {
height: 50dvh;
}
}
&__cover-img {
width: 100%;
height: 100%;
object-fit: cover;
}
&__cover-placeholder {
width: 100%;
height: 100%;
background: var(--color-surface-3);
}
&__img-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(13, 13, 13, 0.6);
border: none;
color: var(--color-cream);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background var(--transition-fast);
backdrop-filter: blur(8px);
&:hover {
background: rgba(13, 13, 13, 0.85);
}
&--prev {
left: 12px;
}
&--next {
right: 12px;
}
}
&__back {
position: absolute;
top: 16px;
left: 16px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(13, 13, 13, 0.6);
border: none;
color: var(--color-cream);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
transition: background var(--transition-fast);
&:hover {
background: rgba(13, 13, 13, 0.85);
}
}
&__content {
padding: 24px;
max-width: 640px;
margin: 0 auto;
}
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
}
&__name {
font-family: var(--font-display);
font-size: 2rem;
color: var(--color-cream);
margin: 0 0 4px;
}
&__age {
opacity: 0.6;
font-size: 1.5rem;
}
&__location {
color: var(--color-muted);
}
&__actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
&__bio {
font-family: var(--font-mono);
font-size: 0.875rem;
color: rgba(240, 235, 224, 0.8);
line-height: 1.65;
margin: 0 0 20px;
}
&__stats {
display: flex;
gap: 24px;
margin-bottom: 20px;
flex-wrap: wrap;
}
&__stat {
display: flex;
flex-direction: column;
gap: 3px;
span:last-child {
font-family: var(--font-mono);
font-size: 0.9375rem;
color: var(--color-cream);
}
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
&__tag {
padding: 4px 12px;
background: rgba(240, 235, 224, 0.06);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
font-family: var(--font-mono);
font-size: 0.6875rem;
color: var(--color-muted);
}
}
</style>