✨ 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
331 lines
8.1 KiB
Vue
331 lines
8.1 KiB
Vue
<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>
|