Files
webapp/app/pages/catalog/hubs/[id]/index.vue
Ruslan Bakiev da29a354ff
All checks were successful
Build Docker Image / build (push) Successful in 3m52s
Add navigation badges to offers and hubs catalog pages
- /catalog/offers/[productId] - badge "Товар: Name"
- /catalog/offers/[productId]/[hubId] - badges "Товар", "Хаб"
- /catalog/hubs/[id] - badge "Хаб: Name"
- /catalog/hubs/[id]/[productId] - badges "Хаб", "Товар"

Removed breadcrumb components, replaced with search bar badges
2026-01-19 11:33:36 +07:00

176 lines
5.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<CatalogPage
:items="filteredProducts"
:map-items="mapItems"
:loading="isLoading"
:total-count="products.length"
with-map
map-id="hub-products-map"
point-color="#10b981"
>
<template #searchBar="{ displayedCount, totalCount }">
<CatalogSearchBar
v-model:search-query="searchQuery"
:active-filters="navigationFilters"
:displayed-count="displayedCount"
:total-count="totalCount"
@remove-filter="handleRemoveFilter"
/>
</template>
<template #header>
<!-- Not Found -->
<Card v-if="!isLoading && !hub" padding="lg">
<Stack align="center" gap="4">
<IconCircle tone="primary">
<Icon name="lucide:map-pin" size="24" />
</IconCircle>
<Heading :level="2">{{ t('catalogHub.not_found.title') }}</Heading>
<Text tone="muted">{{ t('catalogHub.not_found.subtitle') }}</Text>
<Button @click="navigateTo(localePath('/catalog'))">
{{ t('catalogHub.actions.back_to_catalog') }}
</Button>
</Stack>
</Card>
<!-- Content Header -->
<Stack v-else gap="4">
<!-- Header -->
<div>
<Heading :level="1">{{ hub?.name }}</Heading>
<Text tone="muted" size="sm">{{ hub?.country }}</Text>
</div>
</Stack>
</template>
<template #card="{ item }">
<HubProductCard
:name="item.name"
:price-history="getMockPriceHistory(item.uuid)"
@select="goToProduct(item.uuid)"
/>
</template>
<template #empty>
<Stack align="center" gap="2">
<Icon name="lucide:package-x" size="32" class="text-base-content/40" />
<Text tone="muted">{{ t('catalogHub.products.empty') }}</Text>
</Stack>
</template>
</CatalogPage>
</template>
<script setup lang="ts">
import { GetNodeConnectionsDocument, GetProductsNearHubDocument } from '~/composables/graphql/public/geo-generated'
definePageMeta({
layout: 'topnav'
})
const route = useRoute()
const localePath = useLocalePath()
const { t } = useI18n()
const isLoading = ref(true)
const hub = ref<any>(null)
const products = ref<Array<{ uuid: string; name: string }>>([])
const hubId = computed(() => route.params.id as string)
// Navigation filters for search bar badges
const navigationFilters = computed(() => {
const filters: Array<{ id: string; label: string; key: string }> = []
if (hub.value?.name) {
filters.push({
id: 'hub',
key: 'Хаб',
label: hub.value.name
})
}
return filters
})
// Handle removing navigation filter (navigate back)
const handleRemoveFilter = (filterId: string) => {
if (filterId === 'hub') {
navigateTo(localePath('/catalog/hubs'))
}
}
// Search
const searchQuery = ref('')
const filteredProducts = computed(() => {
if (!searchQuery.value.trim()) return products.value
const q = searchQuery.value.toLowerCase()
return products.value.filter(item =>
item.name?.toLowerCase().includes(q)
)
})
// Map items - show the hub itself
const mapItems = computed(() => {
if (!hub.value?.latitude || !hub.value?.longitude) return []
return [{
uuid: hub.value.uuid || hubId.value,
name: hub.value.name || '',
latitude: Number(hub.value.latitude),
longitude: Number(hub.value.longitude),
country: hub.value.country
}]
})
// Navigate to product page
const goToProduct = (productId: string) => {
navigateTo(localePath(`/catalog/hubs/${hubId.value}/${productId}`))
}
// Mock price history generator (seeded by uuid for consistent results)
const getMockPriceHistory = (uuid: string): number[] => {
const seed = uuid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
const basePrice = 100 + (seed % 200)
return Array.from({ length: 7 }, (_, i) => {
const variation = Math.sin(seed + i * 0.5) * 20 + Math.cos(seed * 0.3 + i) * 10
return Math.round(basePrice + variation)
})
}
// Initial load
try {
const [{ data: connectionsData }, { data: productsData }] = await Promise.all([
useServerQuery('hub-connections', GetNodeConnectionsDocument, { uuid: hubId.value }, 'public', 'geo'),
useServerQuery('products-near-hub', GetProductsNearHubDocument, { hubUuid: hubId.value, radiusKm: 500 }, 'public', 'geo')
])
hub.value = connectionsData.value?.nodeConnections?.hub || null
// Get products near this hub (from geo)
products.value = (productsData.value?.productsNearHub || [])
.filter((p): p is { uuid: string; name?: string | null } => p !== null && !!p.uuid)
.map(p => ({ uuid: p.uuid!, name: p.name || '' }))
} catch (error) {
console.error('Error loading hub:', error)
} finally {
isLoading.value = false
}
// SEO
useHead(() => ({
title: hub.value?.name
? t('catalogHub.meta.title_with_name', { name: hub.value.name })
: t('catalogHub.meta.title'),
meta: [
{
name: 'description',
content: t('catalogHub.meta.description', {
name: hub.value?.name || '',
country: hub.value?.country || '',
products: products.value.length
})
}
]
}))
</script>