init
This commit is contained in:
311
src/views/profile/ProfileDetailView.vue
Normal file
311
src/views/profile/ProfileDetailView.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<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 { apiClient } from '@/api/client';
|
||||
import type { UserProfile } from '@/stores/auth.store';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
import AppModal from '@/components/common/AppModal.vue';
|
||||
import ReportModal from '@/components/reports/ReportModal.vue';
|
||||
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 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) as unknown as UserProfile;
|
||||
profile.value = res;
|
||||
} catch {
|
||||
uiStore.addToast('Не удалось загрузить профиль', 'error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
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(() => 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 currentImageIndex = ref(0);
|
||||
|
||||
const isOwnProfile = computed(() =>
|
||||
authStore.profiles.some((p) => p.id === profileId),
|
||||
);
|
||||
</script>
|
||||
|
||||
<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.mediaUrls?.[currentImageIndex]"
|
||||
:src="profile.mediaUrls[currentImageIndex]"
|
||||
: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"
|
||||
@click="currentImageIndex--"
|
||||
aria-label="Предыдущее фото"
|
||||
>‹</button>
|
||||
<button
|
||||
v-if="currentImageIndex < (profile.mediaUrls?.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="Назад">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user