All checks were successful
Build Docker Image / build (push) Successful in 4m55s
Add CatalogSearchBar component with filtering to: - /catalog/offers - search by product name - /catalog/offers/[productId] - search by hub name/country - /catalog/hubs/[id] - search by product name - /catalog/suppliers/[supplierId] - search by product name - /catalog/suppliers/[supplierId]/[productId] - search by hub name/country
158 lines
4.9 KiB
Vue
158 lines
4.9 KiB
Vue
<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"
|
|
:displayed-count="displayedCount"
|
|
:total-count="totalCount"
|
|
/>
|
|
</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">
|
|
<!-- Breadcrumbs -->
|
|
<CatalogBreadcrumbs :hub-id="hubId" :hub-name="hub?.name" />
|
|
|
|
<!-- 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, FindProductsForHubDocument } from '~/composables/graphql/public/geo-generated'
|
|
import { GetAvailableProductsDocument } from '~/composables/graphql/public/exchange-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)
|
|
|
|
// 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: productUuidsData }, { data: allProductsData }] = await Promise.all([
|
|
useServerQuery('hub-connections', GetNodeConnectionsDocument, { uuid: hubId.value }, 'public', 'geo'),
|
|
useServerQuery('hub-product-uuids', FindProductsForHubDocument, { hubUuid: hubId.value }, 'public', 'geo'),
|
|
useServerQuery('available-products', GetAvailableProductsDocument, {}, 'public', 'exchange')
|
|
])
|
|
|
|
hub.value = connectionsData.value?.nodeConnections?.hub || null
|
|
|
|
// Filter products by UUIDs that can be delivered to this hub
|
|
const relevantUuids = new Set(productUuidsData.value?.findProductsForHub?.filter(Boolean) || [])
|
|
products.value = (allProductsData.value?.getAvailableProducts || [])
|
|
.filter((p): p is { uuid: string; name: string } => p !== null && !!p.uuid && !!p.name && relevantUuids.has(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>
|