This commit is contained in:
Oscar
2026-06-08 13:23:20 +03:00
commit 637dddf656
160 changed files with 56097 additions and 0 deletions

View 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>