Files
webapp/app/pages/catalog/hubs/[id]/index.vue
Ruslan Bakiev 29c34a048a
All checks were successful
Build Docker Image / build (push) Successful in 5m0s
fix: migrate geo GraphQL queries and frontend to camelCase
Geo backend was migrated to camelCase but frontend .graphql files and
component code still used snake_case, causing 400 errors on all geo API calls.
2026-03-10 14:10:23 +07:00

190 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"
:hovered-id="hoveredId"
@update:hovered-id="hoveredId = $event"
>
<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>
<Text v-if="!isLoading && !hub" tone="muted">Хаб не найден</Text>
<Text v-else-if="!isLoading" tone="muted">Выберите товар</Text>
</template>
<template #card="{ item }">
<ProductCard
:product="item"
:price-history="getMockPriceHistory(item.uuid)"
selectable
@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 { GetNodeDocument, NearestOffersDocument, type OfferWithRoute, type GetNodeQueryResult } from '~/composables/graphql/public/geo-generated'
type Hub = NonNullable<GetNodeQueryResult['node']>
definePageMeta({
layout: 'topnav'
})
const route = useRoute()
const localePath = useLocalePath()
const { t } = useI18n()
const isLoading = ref(true)
const hoveredId = ref<string>()
const hub = ref<Hub | null>(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?select=hub'))
}
}
// 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 unified catalog with hub + product = offers
const goToProduct = (productId: string) => {
navigateTo(localePath(`/catalog?hub=${hubId.value}&product=${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 {
// First load hub node to get coordinates
const { data: hubData } = await useServerQuery(
'hub-node',
GetNodeDocument,
{ uuid: hubId.value },
'public',
'geo'
)
hub.value = hubData.value?.node || null
// Load offers near hub and group by product
if (hub.value?.latitude && hub.value?.longitude) {
const { data: offersData } = await useServerQuery(
'offers-near-hub',
NearestOffersDocument,
{
lat: hub.value.latitude,
lon: hub.value.longitude,
radius: 500
},
'public',
'geo'
)
// Group offers by product
const productsMap = new Map<string, { uuid: string; name: string }>()
offersData.value?.nearestOffers?.forEach((offer) => {
if (offer?.productUuid) {
if (!productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, {
uuid: offer.productUuid,
name: offer.productName || ''
})
}
}
})
products.value = Array.from(productsMap.values())
}
} 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>