211 lines
5.7 KiB
Vue
211 lines
5.7 KiB
Vue
<template>
|
||
<div class="object-detail">
|
||
<div v-if="loading" class="container section">
|
||
<div class="skeleton" style="height: 400px; margin-bottom: 24px" />
|
||
<div class="skeleton" style="height: 32px; width: 60%; margin-bottom: 16px" />
|
||
<div class="skeleton" style="height: 16px; width: 40%; margin-bottom: 8px" />
|
||
<div class="skeleton" style="height: 100px" />
|
||
</div>
|
||
|
||
<template v-else-if="object">
|
||
<section class="object-gallery">
|
||
<div class="container">
|
||
<div class="object-gallery__main">
|
||
<img
|
||
v-if="object.images?.[0]"
|
||
:src="object.images[0]"
|
||
:alt="object.title"
|
||
/>
|
||
<div v-else class="object-gallery__placeholder">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5" stroke="currentColor" stroke-width="2"/>
|
||
<path d="M21 15l-5-5L5 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section">
|
||
<div class="container">
|
||
<div class="object-layout">
|
||
<div class="object-content">
|
||
<div class="object-header">
|
||
<h1>{{ object.title }}</h1>
|
||
<div class="object-header__meta">
|
||
<span v-if="object.category_name" class="tag">{{ object.category_name }}</span>
|
||
<span v-if="object.rating" class="object-rating">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="#FF8833">
|
||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||
</svg>
|
||
{{ object.rating.toFixed(1) }}
|
||
<span v-if="object.review_count">({{ object.review_count }})</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="object.location" class="object-location">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" stroke="currentColor" stroke-width="2"/>
|
||
<circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="2"/>
|
||
</svg>
|
||
{{ object.location }}
|
||
</div>
|
||
|
||
<div class="object-description">
|
||
<h3>Описание</h3>
|
||
<p class="body-text">{{ object.description }}</p>
|
||
</div>
|
||
|
||
<div class="object-reviews">
|
||
<h3>Отзывы</h3>
|
||
<p class="small-text">Здесь будут отзывы о месте</p>
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="object-sidebar">
|
||
<div class="object-actions">
|
||
<button class="btn btn--secondary btn--full btn--md">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" stroke="currentColor" stroke-width="2" fill="none"/>
|
||
</svg>
|
||
В избранное
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<div v-else-if="error" class="container section">
|
||
<p class="body-text--gray">Объект не найден</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
definePageMeta({
|
||
title: 'Объект',
|
||
})
|
||
|
||
const route = useRoute()
|
||
const objectsStore = useObjectsStore()
|
||
const { currentObject: object, loading, error } = storeToRefs(objectsStore)
|
||
|
||
const id = computed(() => Number(route.params.id))
|
||
|
||
onMounted(() => {
|
||
if (id.value) {
|
||
objectsStore.fetchObject(id.value)
|
||
}
|
||
})
|
||
|
||
watch(id, (newId) => {
|
||
if (newId) {
|
||
objectsStore.fetchObject(newId)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.object-gallery {
|
||
background: var(--color-bg-gray);
|
||
}
|
||
|
||
.object-gallery__main {
|
||
max-height: 400px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.object-gallery__main img {
|
||
width: 100%;
|
||
height: 400px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.object-gallery__placeholder {
|
||
height: 400px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--color-text-gray);
|
||
}
|
||
|
||
.object-layout {
|
||
display: grid;
|
||
grid-template-columns: 1fr 280px;
|
||
gap: 40px;
|
||
align-items: start;
|
||
}
|
||
|
||
.object-header {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.object-header h1 {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.object-header__meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.object-rating {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-family: var(--font-body);
|
||
font-size: var(--font-size-body);
|
||
font-weight: var(--font-weight-semibold);
|
||
color: var(--color-text-black);
|
||
}
|
||
|
||
.object-location {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-family: var(--font-body);
|
||
font-size: var(--font-size-body);
|
||
color: var(--color-text-gray);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.object-description {
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.object-description h3 {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.object-reviews h3 {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.object-sidebar {
|
||
position: sticky;
|
||
top: calc(var(--header-height) + 24px);
|
||
}
|
||
|
||
.object-actions {
|
||
background: var(--color-white);
|
||
border: 1px solid var(--color-stroke);
|
||
border-radius: var(--radius-md);
|
||
padding: 20px;
|
||
}
|
||
|
||
@media (max-width: 744px) {
|
||
.object-layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.object-sidebar {
|
||
position: static;
|
||
}
|
||
}
|
||
</style>
|