Refactor: use topnav layout + CatalogPage component
All checks were successful
Build Docker Image / build (push) Successful in 4m11s

- Remove catalog.vue layout and useCatalogLayout.ts (broken provide/inject)
- All catalog/clientarea list pages now use topnav layout
- Pages use CatalogPage component for SearchBar + Map functionality
- Clean architecture: layout handles nav, component handles features
This commit is contained in:
Ruslan Bakiev
2026-01-15 11:18:57 +07:00
parent 7ea96a97b3
commit 03485b77a5
8 changed files with 401 additions and 1092 deletions

View File

@@ -1,29 +1,49 @@
<template>
<div>
<!-- List content -->
<div v-if="isLoading || productsLoading" class="flex items-center justify-center py-8">
<Card padding="lg">
<Stack align="center" justify="center" gap="3">
<Spinner />
<Text tone="muted">{{ t('catalogLanding.states.loading') }}</Text>
</Stack>
</Card>
</div>
<CatalogPage
:items="displayItems"
:map-items="itemsWithCoords"
:loading="isLoading || productsLoading"
with-map
map-id="offers-map"
point-color="#f59e0b"
:selected-id="selectedOfferId"
:hovered-id="hoveredOfferId"
:total-count="total"
@select="onSelectOffer"
@update:hovered-id="hoveredOfferId = $event"
>
<template #searchBar="{ displayedCount, totalCount }">
<CatalogSearchBar
v-model:search-query="searchQuery"
:active-filters="activeFilterBadges"
:displayed-count="displayedCount"
:total-count="totalCount"
@remove-filter="onRemoveFilter"
@search="onSearch"
>
<template #filters>
<div class="p-2 space-y-3">
<div>
<div class="text-xs font-semibold mb-1 text-base-content/70">{{ t('catalogOffersSection.filters.product') }}</div>
<ul class="menu menu-compact max-h-48 overflow-y-auto">
<li v-for="filter in productFilters" :key="filter.id">
<a
:class="{ 'active': (selectedProductUuid === filter.id || (!selectedProductUuid && filter.id === 'all')) }"
@click="onProductFilterChange(filter.id)"
>{{ filter.label }}</a>
</li>
</ul>
</div>
</div>
</template>
</CatalogSearchBar>
</template>
<template v-else>
<Stack gap="3">
<div
v-for="item in displayItems"
:key="item.uuid"
:class="{ 'ring-2 ring-primary rounded-lg': item.uuid === selectedOfferId }"
@click="onSelectOffer(item)"
@mouseenter="hoveredOfferId = item.uuid"
@mouseleave="hoveredOfferId = undefined"
>
<OfferCard :offer="item" />
</div>
</Stack>
<template #card="{ item }">
<OfferCard :offer="item" />
</template>
<template #pagination>
<PaginationLoadMore
v-if="displayItems.length > 0"
:shown="displayItems.length"
@@ -34,21 +54,19 @@
@load-more="loadMore"
class="mt-4"
/>
<Stack v-if="displayItems.length === 0" align="center" gap="2" class="py-8">
<Text tone="muted">{{ t('catalogOffersSection.empty.no_offers') }}</Text>
</Stack>
</template>
</div>
<template #empty>
<Text tone="muted">{{ t('catalogOffersSection.empty.no_offers') }}</Text>
</template>
</CatalogPage>
</template>
<script setup lang="ts">
import { h, defineComponent } from 'vue'
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
import { provideCatalogLayout } from '~/composables/useCatalogLayout'
definePageMeta({
layout: 'catalog'
layout: 'topnav'
})
const { t } = useI18n()
@@ -84,10 +102,6 @@ const searchQuery = ref('')
const searchWithMap = ref(false)
const currentBounds = ref<MapBounds | null>(null)
const onBoundsChange = (bounds: MapBounds) => {
currentBounds.value = bounds
}
// Map items with correct coordinate field names
const itemsWithCoords = computed(() =>
items.value.filter(item =>
@@ -115,14 +129,6 @@ const displayItems = computed(() => {
})
})
// Hovered item with coordinates for map highlight
const hoveredItem = computed(() => {
if (!hoveredOfferId.value) return null
const item = items.value.find(i => i.uuid === hoveredOfferId.value)
if (!item?.locationLatitude || !item?.locationLongitude) return null
return { latitude: Number(item.locationLatitude), longitude: Number(item.locationLongitude) }
})
// Product filter options
const productFilters = computed(() => {
const all = [{ id: 'all', label: t('catalogOffersSection.filters.all_products') }]
@@ -164,54 +170,6 @@ const onSelectOffer = (offer: any) => {
selectedOfferId.value = offer.uuid
}
// Handle selection from map
const onMapSelect = (uuid: string) => {
const offer = items.value.find(i => i.uuid === uuid)
if (offer) {
selectedOfferId.value = uuid
}
}
// Filter component for the layout
const OffersFilterComponent = defineComponent({
setup() {
return () => h('div', { class: 'p-2 space-y-3' }, [
h('div', {}, [
h('div', { class: 'text-xs font-semibold mb-1 text-base-content/70' }, t('catalogOffersSection.filters.product')),
h('ul', { class: 'menu menu-compact max-h-48 overflow-y-auto' },
productFilters.value.map(filter =>
h('li', { key: filter.id },
h('a', {
class: (selectedProductUuid.value === filter.id || (!selectedProductUuid.value && filter.id === 'all')) ? 'active' : '',
onClick: () => onProductFilterChange(filter.id)
}, filter.label)
)
)
)
])
])
}
})
// Provide data to layout
provideCatalogLayout({
searchQuery,
activeFilters: activeFilterBadges,
displayedCount: computed(() => displayItems.value.length),
totalCount: computed(() => total.value),
onSearch,
onRemoveFilter,
filterComponent: OffersFilterComponent,
mapItems: itemsWithCoords,
mapId: 'offers-map',
pointColor: '#f59e0b',
hoveredItemId: hoveredOfferId,
hoveredItem,
onMapSelect,
onBoundsChange,
searchWithMap
})
// Initialize
await Promise.all([initProducts(), initOffers()])