Initial commit from monorepo
This commit is contained in:
310
app/pages/catalog/hubs/[id].vue
Normal file
310
app/pages/catalog/hubs/[id].vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<Stack gap="0">
|
||||
<!-- Loading -->
|
||||
<Section v-if="isLoading" variant="plain" paddingY="lg">
|
||||
<Stack align="center" justify="center" gap="4">
|
||||
<Spinner />
|
||||
<Text tone="muted">{{ t('catalogHub.states.loading') }}</Text>
|
||||
</Stack>
|
||||
</Section>
|
||||
|
||||
<!-- Error / Not Found -->
|
||||
<Section v-else-if="!hub" variant="plain" paddingY="lg">
|
||||
<Card 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>
|
||||
</Section>
|
||||
|
||||
<template v-else>
|
||||
<!-- Map Hero -->
|
||||
<MapHero
|
||||
:title="hub.name"
|
||||
:location="mapLocation"
|
||||
:badges="hubBadges"
|
||||
/>
|
||||
|
||||
<!-- Offers Section -->
|
||||
<Section v-if="offers.length > 0" variant="plain" paddingY="md">
|
||||
<Stack gap="4">
|
||||
<Heading :level="2">{{ t('catalogHub.sections.offers.title') }}</Heading>
|
||||
<Grid :cols="1" :md="2" :lg="3" :gap="4">
|
||||
<OfferCard
|
||||
v-for="offer in offers"
|
||||
:key="offer.uuid"
|
||||
:offer="offer"
|
||||
/>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Section>
|
||||
|
||||
<!-- Empty offers state -->
|
||||
<Section v-else variant="plain" paddingY="md">
|
||||
<Card padding="lg">
|
||||
<Stack align="center" gap="4">
|
||||
<IconCircle tone="primary">
|
||||
<Icon name="lucide:package-x" size="24" />
|
||||
</IconCircle>
|
||||
<Heading :level="3">{{ t('catalogHub.empty.offers.title') }}</Heading>
|
||||
<Text tone="muted" align="center">
|
||||
{{ t('catalogHub.empty.offers.subtitle') }}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<!-- Suppliers Section -->
|
||||
<Section v-if="uniqueSuppliers.length > 0" variant="plain" paddingY="md">
|
||||
<Stack gap="4">
|
||||
<Heading :level="2">{{ t('catalogHub.sections.suppliers.title') }}</Heading>
|
||||
<Grid :cols="1" :md="2" :lg="3" :gap="4">
|
||||
<SupplierCard
|
||||
v-for="supplier in uniqueSuppliers"
|
||||
:key="supplier.teamUuid"
|
||||
:supplier="supplier"
|
||||
/>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Section>
|
||||
|
||||
<!-- Products Section -->
|
||||
<Section v-if="uniqueProducts.length > 0" variant="plain" paddingY="md">
|
||||
<Stack gap="4">
|
||||
<Heading :level="2">{{ t('catalogHub.sections.products.title') }}</Heading>
|
||||
<Stack direction="row" gap="2" wrap>
|
||||
<NuxtLink
|
||||
v-for="product in uniqueProducts"
|
||||
:key="product.uuid"
|
||||
:to="localePath(`/catalog/products/${product.uuid}`)"
|
||||
>
|
||||
<Pill variant="primary" class="hover:bg-primary hover:text-white transition-colors cursor-pointer">
|
||||
{{ product.name }}
|
||||
</Pill>
|
||||
</NuxtLink>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Section>
|
||||
|
||||
<!-- Nearby Connections -->
|
||||
<NearbyConnectionsSection
|
||||
:auto-edges="autoEdges"
|
||||
:rail-edges="railEdges"
|
||||
:hub="currentHubForMap"
|
||||
:rail-hub="railHubForMap"
|
||||
:auto-route-geometries="autoRouteGeometries"
|
||||
:rail-route-geometries="railRouteGeometries"
|
||||
/>
|
||||
|
||||
</template>
|
||||
</Stack>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetLocationOffersDocument, GetSupplierProfilesDocument } from '~/composables/graphql/public/exchange-generated'
|
||||
import { GetNodeConnectionsDocument, GetAutoRouteDocument, GetRailRouteDocument } from '~/composables/graphql/public/geo-generated'
|
||||
import type { EdgeType } from '~/composables/graphql/public/geo-generated'
|
||||
|
||||
interface RouteGeometry {
|
||||
toUuid: string
|
||||
coordinates: [number, number][]
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const localePath = useLocalePath()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(true)
|
||||
const hub = ref<any>(null)
|
||||
const railHub = ref<any>(null)
|
||||
const offers = ref<any[]>([])
|
||||
const allSuppliers = ref<any[]>([])
|
||||
const autoEdges = ref<EdgeType[]>([])
|
||||
const railEdges = ref<EdgeType[]>([])
|
||||
const autoRouteGeometries = ref<RouteGeometry[]>([])
|
||||
const railRouteGeometries = ref<RouteGeometry[]>([])
|
||||
|
||||
const hubId = computed(() => route.params.id as string)
|
||||
|
||||
// Map location
|
||||
const mapLocation = computed(() => ({
|
||||
uuid: hub.value?.uuid,
|
||||
name: hub.value?.name,
|
||||
latitude: hub.value?.latitude,
|
||||
longitude: hub.value?.longitude,
|
||||
country: hub.value?.country,
|
||||
countryCode: hub.value?.countryCode
|
||||
}))
|
||||
|
||||
// Badges for MapHero
|
||||
const hubBadges = computed(() => {
|
||||
const badges: Array<{ icon?: string; text: string }> = []
|
||||
if (hub.value?.country) {
|
||||
badges.push({ icon: 'lucide:globe', text: hub.value.country })
|
||||
}
|
||||
if (offers.value.length > 0) {
|
||||
badges.push({ icon: 'lucide:package', text: t('catalogHub.badges.offers', { count: offers.value.length }) })
|
||||
}
|
||||
if (hub.value?.latitude && hub.value?.longitude) {
|
||||
badges.push({ icon: 'lucide:map-pin', text: `${hub.value.latitude.toFixed(2)}°, ${hub.value.longitude.toFixed(2)}°` })
|
||||
}
|
||||
return badges
|
||||
})
|
||||
|
||||
// Unique suppliers
|
||||
const uniqueSuppliers = computed(() => {
|
||||
const suppliers = new Map<string, { teamUuid: string; name: string; offersCount: number }>()
|
||||
offers.value.forEach(offer => {
|
||||
if (offer.teamUuid) {
|
||||
const existing = suppliers.get(offer.teamUuid)
|
||||
const supplierInfo = allSuppliers.value.find(s => s.teamUuid === offer.teamUuid)
|
||||
if (existing) {
|
||||
existing.offersCount++
|
||||
} else {
|
||||
suppliers.set(offer.teamUuid, {
|
||||
teamUuid: offer.teamUuid,
|
||||
name: supplierInfo?.name || t('catalogHub.labels.default_supplier'),
|
||||
offersCount: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return Array.from(suppliers.values())
|
||||
})
|
||||
|
||||
// Unique products
|
||||
const uniqueProducts = computed(() => {
|
||||
const products = new Map<string, { uuid: string; name: string }>()
|
||||
offers.value.forEach(offer => {
|
||||
offer.lines?.forEach((line: any) => {
|
||||
if (line?.productUuid && line?.productName) {
|
||||
products.set(line.productUuid, { uuid: line.productUuid, name: line.productName })
|
||||
}
|
||||
})
|
||||
})
|
||||
return Array.from(products.values())
|
||||
})
|
||||
|
||||
// Current hub for NearbyConnectionsSection
|
||||
const currentHubForMap = computed(() => ({
|
||||
uuid: hub.value?.uuid || '',
|
||||
name: hub.value?.name || '',
|
||||
latitude: hub.value?.latitude || 0,
|
||||
longitude: hub.value?.longitude || 0
|
||||
}))
|
||||
|
||||
const railHubForMap = computed(() => ({
|
||||
uuid: railHub.value?.uuid || hub.value?.uuid || '',
|
||||
name: railHub.value?.name || hub.value?.name || '',
|
||||
latitude: railHub.value?.latitude || hub.value?.latitude || 0,
|
||||
longitude: railHub.value?.longitude || hub.value?.longitude || 0
|
||||
}))
|
||||
|
||||
// Load route geometries for edges
|
||||
const loadRouteGeometries = async (
|
||||
edges: EdgeType[],
|
||||
hubLat: number,
|
||||
hubLon: number,
|
||||
transportType: 'auto' | 'rail'
|
||||
): Promise<RouteGeometry[]> => {
|
||||
const RouteDocument = transportType === 'auto' ? GetAutoRouteDocument : GetRailRouteDocument
|
||||
const routeField = transportType === 'auto' ? 'autoRoute' : 'railRoute'
|
||||
|
||||
const filteredEdges = edges
|
||||
.filter(e => e?.transportType === transportType && e?.toLatitude && e?.toLongitude)
|
||||
.sort((a, b) => (a.distanceKm || 0) - (b.distanceKm || 0))
|
||||
.slice(0, 12)
|
||||
|
||||
const routePromises = filteredEdges.map(async (edge) => {
|
||||
try {
|
||||
const { data: routeDataResponse } = await useServerQuery(
|
||||
`hub-route-${transportType}-${edge.toUuid}`,
|
||||
RouteDocument,
|
||||
{
|
||||
fromLat: hubLat,
|
||||
fromLon: hubLon,
|
||||
toLat: edge.toLatitude!,
|
||||
toLon: edge.toLongitude!
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
)
|
||||
|
||||
const routeData = routeDataResponse.value?.[routeField]
|
||||
if (routeData?.geometry) {
|
||||
const geometryArray = typeof routeData.geometry === 'string'
|
||||
? JSON.parse(routeData.geometry)
|
||||
: routeData.geometry
|
||||
|
||||
if (Array.isArray(geometryArray) && geometryArray.length > 0) {
|
||||
return {
|
||||
toUuid: edge.toUuid!,
|
||||
coordinates: geometryArray as [number, number][]
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${transportType} route to ${edge.toName}:`, error)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const results = await Promise.all(routePromises)
|
||||
return results.filter(Boolean) as RouteGeometry[]
|
||||
}
|
||||
|
||||
try {
|
||||
const [{ data: connectionsData }, { data: offersData }, { data: suppliersData }] = await Promise.all([
|
||||
useServerQuery('hub-connections', GetNodeConnectionsDocument, { uuid: hubId.value }, 'public', 'geo'),
|
||||
useServerQuery('hub-offers', GetLocationOffersDocument, { locationUuid: hubId.value }, 'public', 'exchange'),
|
||||
useServerQuery('hub-suppliers', GetSupplierProfilesDocument, {}, 'public', 'exchange')
|
||||
])
|
||||
|
||||
const connectionsResult = connectionsData.value
|
||||
hub.value = connectionsResult?.nodeConnections?.hub || null
|
||||
railHub.value = connectionsResult?.nodeConnections?.railNode || null
|
||||
offers.value = offersData.value?.getOffers || []
|
||||
allSuppliers.value = suppliersData.value?.getSupplierProfiles || []
|
||||
autoEdges.value = (connectionsResult?.nodeConnections?.autoEdges || []).filter((e): e is EdgeType => e !== null)
|
||||
railEdges.value = (connectionsResult?.nodeConnections?.railEdges || []).filter((e): e is EdgeType => e !== null)
|
||||
|
||||
if (hub.value?.latitude && hub.value?.longitude) {
|
||||
const railOrigin = railHub.value || hub.value
|
||||
const [autoGeometries, railGeometries] = await Promise.all([
|
||||
loadRouteGeometries(autoEdges.value, hub.value.latitude, hub.value.longitude, 'auto'),
|
||||
loadRouteGeometries(railEdges.value, railOrigin.latitude, railOrigin.longitude, 'rail')
|
||||
])
|
||||
autoRouteGeometries.value = autoGeometries
|
||||
railRouteGeometries.value = railGeometries
|
||||
}
|
||||
} 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 || '',
|
||||
offers: offers.value.length,
|
||||
suppliers: uniqueSuppliers.value.length
|
||||
})
|
||||
}
|
||||
]
|
||||
}))
|
||||
</script>
|
||||
Reference in New Issue
Block a user