diff --git a/app/components/catalog/CatalogBreadcrumbs.vue b/app/components/catalog/CatalogBreadcrumbs.vue new file mode 100644 index 0000000..3cc2459 --- /dev/null +++ b/app/components/catalog/CatalogBreadcrumbs.vue @@ -0,0 +1,51 @@ + + + + + + {{ crumb.label }} + + {{ crumb.label }} + + + + + + diff --git a/app/pages/catalog/hubs/[id].vue b/app/pages/catalog/hubs/[id]/[productId].vue similarity index 55% rename from app/pages/catalog/hubs/[id].vue rename to app/pages/catalog/hubs/[id]/[productId].vue index 5c051b8..1ec1536 100644 --- a/app/pages/catalog/hubs/[id].vue +++ b/app/pages/catalog/hubs/[id]/[productId].vue @@ -9,56 +9,70 @@ - + - + - {{ t('catalogHub.not_found.title') }} - {{ t('catalogHub.not_found.subtitle') }} - - {{ t('catalogHub.actions.back_to_catalog') }} + {{ t('catalogHub.product_not_found.title') }} + {{ t('catalogHub.product_not_found.subtitle') }} + + {{ t('catalogHub.actions.back_to_hub') }} + - - - {{ hub.name }} - {{ hub.country }} - - - - - - - + + - + + + + {{ product.name }} + {{ hub.name }}, {{ hub.country }} + + + + + + {{ t('catalogHub.product.priceHistory') }} + + + + + + + + - {{ t('catalogHub.sources.empty') }} + + + {{ t('catalogHub.sources.empty') }} + @@ -92,31 +109,81 @@ const { execute } = useGraphQL() const isLoading = ref(true) const isLoadingRoutes = ref(false) const hub = ref(null) -const products = ref>([]) -const selectedProductUuid = ref('') +const product = ref<{ uuid: string; name: string } | null>(null) const selectedSourceUuid = ref('') const rawSources = ref([]) const offersData = ref>(new Map()) +const hubId = computed(() => route.params.id as string) +const productId = computed(() => route.params.productId as string) + // 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 Array.from({ length: 30 }, (_, i) => { + const variation = Math.sin(seed + i * 0.3) * 30 + Math.cos(seed * 0.2 + i) * 15 return Math.round(basePrice + variation) }) } -const hubId = computed(() => route.params.id as string) +// Chart configuration +const priceHistory = computed(() => getMockPriceHistory(productId.value)) -// Selected product name -const selectedProductName = computed(() => { - const product = products.value.find(p => p.uuid === selectedProductUuid.value) - return product?.name || '' +const trend = computed(() => { + if (priceHistory.value.length < 2) return 0 + const first = priceHistory.value[0] + const last = priceHistory.value[priceHistory.value.length - 1] + if (!first || first === 0) return 0 + return Math.round(((last - first) / first) * 100) }) -// Transform sources for CatalogPage (needs uuid, latitude, longitude, name, stages) +const chartOptions = computed(() => ({ + chart: { + type: 'area', + toolbar: { show: false }, + animations: { enabled: true } + }, + stroke: { + curve: 'smooth', + width: 2 + }, + fill: { + type: 'gradient', + gradient: { + shadeIntensity: 1, + opacityFrom: 0.4, + opacityTo: 0.1 + } + }, + colors: [trend.value >= 0 ? '#22c55e' : '#ef4444'], + dataLabels: { enabled: false }, + xaxis: { + categories: priceHistory.value.map((_, i) => `${i + 1}`), + labels: { show: false } + }, + yaxis: { + labels: { + formatter: (val: number) => `$${val}` + } + }, + tooltip: { + y: { + formatter: (val: number) => `$${val}` + } + }, + grid: { + borderColor: '#e5e7eb', + strokeDashArray: 4 + } +})) + +const chartSeries = computed(() => [{ + name: t('catalogHub.product.price'), + data: priceHistory.value +}]) + +// Transform sources for CatalogPage const sources = computed(() => { return rawSources.value.map(source => ({ uuid: source.sourceUuid || '', @@ -158,9 +225,9 @@ const loadOfferDetails = async () => { offersData.value = newOffersData } -// Load routes when product changes +// Load routes const loadRoutes = async () => { - if (!selectedProductUuid.value || !hubId.value) { + if (!productId.value || !hubId.value) { rawSources.value = [] offersData.value.clear() return @@ -173,7 +240,7 @@ const loadRoutes = async () => { const data = await execute( FindProductRoutesDocument, { - productUuid: selectedProductUuid.value, + productUuid: productId.value, toUuid: hubId.value, limitSources: 12, limitRoutes: 1 @@ -191,29 +258,6 @@ const loadRoutes = async () => { } } -watch(selectedProductUuid, loadRoutes) - -// Formatting helpers -const formatDistance = (km: number | null | undefined) => { - if (!km) return '0' - return Math.round(km).toLocaleString() -} - -const formatDuration = (seconds: number | null | undefined) => { - if (!seconds) return '-' - const hours = Math.floor(seconds / 3600) - const minutes = Math.floor((seconds % 3600) / 60) - if (hours > 24) { - const days = Math.floor(hours / 24) - const remainingHours = hours % 24 - return `${days}д ${remainingHours}ч` - } - if (hours > 0) { - return `${hours}ч ${minutes}м` - } - return `${minutes}м` -} - // Initial load try { const [{ data: connectionsData }, { data: productsData }] = await Promise.all([ @@ -222,28 +266,34 @@ try { ]) hub.value = connectionsData.value?.nodeConnections?.hub || null - products.value = (productsData.value?.getAvailableProducts || []) + + const products = (productsData.value?.getAvailableProducts || []) .filter((p): p is { uuid: string; name: string } => p !== null && !!p.uuid && !!p.name) - .map(p => ({ uuid: p.uuid!, name: p.name! })) + + product.value = products.find(p => p.uuid === productId.value) || null + + // Load routes after initial data + if (product.value) { + await loadRoutes() + } } catch (error) { - console.error('Error loading hub:', error) + console.error('Error loading data:', error) } finally { isLoading.value = false } // SEO useHead(() => ({ - title: hub.value?.name - ? t('catalogHub.meta.title_with_name', { name: hub.value.name }) + title: product.value?.name && hub.value?.name + ? `${product.value.name} - ${hub.value.name}` : t('catalogHub.meta.title'), meta: [ { name: 'description', - content: t('catalogHub.meta.description', { - name: hub.value?.name || '', - country: hub.value?.country || '', - offers: sources.value.length, - suppliers: 0 + content: t('catalogHub.product.meta.description', { + product: product.value?.name || '', + hub: hub.value?.name || '', + offers: sources.value.length }) } ] diff --git a/app/pages/catalog/hubs/[id]/index.vue b/app/pages/catalog/hubs/[id]/index.vue new file mode 100644 index 0000000..4d4cfa6 --- /dev/null +++ b/app/pages/catalog/hubs/[id]/index.vue @@ -0,0 +1,126 @@ + + + + + + + {{ t('catalogHub.states.loading') }} + + + + + + + + + + + {{ t('catalogHub.not_found.title') }} + {{ t('catalogHub.not_found.subtitle') }} + + {{ t('catalogHub.actions.back_to_catalog') }} + + + + + + + + + + + + + + {{ hub.name }} + {{ hub.country }} + + + + + + + + + + + {{ t('catalogHub.products.empty') }} + + + + + + +