Refactor catalog layout: replace Teleport with provide/inject
All checks were successful
Build Docker Image / build (push) Successful in 4m1s

- Create useCatalogLayout composable for data transfer from pages to layout
- Layout now owns SearchBar and CatalogMap components directly
- Pages provide data via provideCatalogLayout()
- Fixes navigation glitches (multiple SearchBars) when switching tabs
- Support custom subNavItems for clientarea pages
- Unify 6 pages to use catalog layout:
  - catalog/offers, suppliers, hubs
  - clientarea/orders, addresses, offers
This commit is contained in:
Ruslan Bakiev
2026-01-15 10:49:40 +07:00
parent 4bd5b882e0
commit 7ea96a97b3
8 changed files with 828 additions and 387 deletions

View File

@@ -37,7 +37,7 @@
</div>
</div>
<!-- Layer 3: SearchBar (fixed, slides to top:0) - teleport target -->
<!-- Layer 3: SearchBar (fixed, slides to top:0) -->
<div
class="fixed left-0 right-0 z-40 h-14 bg-base-100 border-b border-base-300 flex items-center"
:style="{ top: `${searchBarTop}px` }"
@@ -49,16 +49,51 @@
>
<Icon :name="isCollapsed ? 'lucide:chevron-down' : 'lucide:chevron-up'" size="16" />
</button>
<!-- SearchBar content teleported here (flex-1 for full width) -->
<div id="catalog-searchbar" class="flex-1 min-w-0" />
<!-- SearchBar from page via inject -->
<div class="flex-1 min-w-0 px-4 lg:px-6 py-2">
<CatalogSearchBar
v-if="catalogData"
v-model:search-query="catalogData.searchQuery.value"
:active-filters="toValue(catalogData.activeFilters)"
:displayed-count="toValue(catalogData.displayedCount)"
:total-count="toValue(catalogData.totalCount)"
@remove-filter="catalogData.onRemoveFilter"
@search="catalogData.onSearch"
>
<template v-if="catalogData.filterComponent" #filters>
<component :is="catalogData.filterComponent" />
</template>
</CatalogSearchBar>
</div>
</div>
<!-- Layer 4: Map (fixed, right side, to bottom) - teleport target -->
<!-- Layer 4: Map (fixed, right side, to bottom) -->
<div
id="catalog-map"
class="fixed right-0 bottom-0 w-3/5 z-20 hidden lg:block"
:style="{ top: `${mapTop}px` }"
/>
>
<div v-if="catalogData" class="h-full w-full relative">
<!-- Search with map checkbox -->
<label class="absolute top-4 left-4 z-10 bg-white/90 backdrop-blur px-3 py-2 rounded-lg shadow flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="catalogData.searchWithMap.value" class="checkbox checkbox-sm" />
<span class="text-sm">{{ t('catalogMap.searchWithMap') }}</span>
</label>
<ClientOnly>
<CatalogMap
ref="mapRef"
:map-id="catalogData.mapId"
:items="catalogData.useServerClustering ? [] : toValue(catalogData.mapItems)"
:clustered-points="catalogData.useServerClustering ? toValue(catalogData.clusteredNodes || []) : []"
:use-server-clustering="catalogData.useServerClustering || false"
:point-color="catalogData.pointColor"
:hovered-item-id="catalogData.hoveredItemId.value"
:hovered-item="toValue(catalogData.hoveredItem)"
@select-item="catalogData.onMapSelect"
@bounds-change="catalogData.onBoundsChange"
/>
</ClientOnly>
</div>
</div>
<!-- Content area (left side, with dynamic padding for fixed header) -->
<div
@@ -90,17 +125,40 @@
</div>
</div>
<!-- Mobile map view - teleport target -->
<!-- Mobile map view -->
<div
v-if="mobileView === 'map'"
id="catalog-map-mobile"
v-if="mobileView === 'map' && catalogData"
class="lg:hidden fixed inset-0 z-20"
:style="{ top: `${mapTop}px` }"
/>
>
<div class="h-full w-full relative">
<!-- Search with map checkbox -->
<label class="absolute top-4 left-4 z-10 bg-white/90 backdrop-blur px-3 py-2 rounded-lg shadow flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="catalogData.searchWithMap.value" class="checkbox checkbox-sm" />
<span class="text-sm">{{ t('catalogMap.searchWithMap') }}</span>
</label>
<ClientOnly>
<CatalogMap
:map-id="`${catalogData.mapId}-mobile`"
:items="catalogData.useServerClustering ? [] : toValue(catalogData.mapItems)"
:clustered-points="catalogData.useServerClustering ? toValue(catalogData.clusteredNodes || []) : []"
:use-server-clustering="catalogData.useServerClustering || false"
:point-color="catalogData.pointColor"
:hovered-item-id="catalogData.hoveredItemId.value"
:hovered-item="toValue(catalogData.hoveredItem)"
@select-item="catalogData.onMapSelect"
@bounds-change="catalogData.onBoundsChange"
/>
</ClientOnly>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { toValue } from 'vue'
import { useCatalogLayoutData } from '~/composables/useCatalogLayout'
const runtimeConfig = useRuntimeConfig()
const siteUrl = runtimeConfig.public.siteUrl || 'https://optovia.ru/'
const { signIn, signOut, loggedIn, fetch: fetchSession } = useAuth()
@@ -108,6 +166,12 @@ const route = useRoute()
const localePath = useLocalePath()
const { t } = useI18n()
// Inject catalog data from page
const catalogData = useCatalogLayoutData()
// Map ref for external flyTo access
const mapRef = ref<{ flyTo: (lat: number, lng: number, zoom?: number) => void } | null>(null)
// Smooth collapsible header
// MainNav: 64px (h-16)
// SubNav: 40px (py-2 + links)
@@ -160,12 +224,16 @@ const userInitials = computed(() => {
return '?'
})
// SubNavigation items for catalog section
const subNavItems = computed(() => [
// SubNavigation items - use page-provided items or default to catalog section
const defaultSubNavItems = [
{ label: t('cabinetNav.offers'), path: '/catalog/offers' },
{ label: t('cabinetNav.suppliers'), path: '/catalog/suppliers' },
{ label: t('cabinetNav.hubs'), path: '/catalog/hubs' },
])
]
const subNavItems = computed(() => {
return catalogData?.subNavItems || defaultSubNavItems
})
const isSubNavActive = (path: string) => {
return route.path === localePath(path) || route.path.startsWith(localePath(path) + '/')