All checks were successful
Build Docker Image / build (push) Successful in 3m45s
- MainNavigation: center tabs on page (absolute positioning) - SubNavigation: align left instead of center - Create CatalogPage universal component for list+map pages - Migrate catalog pages (offers, suppliers, hubs) to CatalogPage - Remove PageHeader from clientarea pages (redundant with navigation) - Add topnav layout to supplier detail page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
256 lines
8.5 KiB
Vue
256 lines
8.5 KiB
Vue
<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('catalogSupplier.states.loading') }}</Text>
|
|
</Stack>
|
|
</Section>
|
|
|
|
<!-- Error / Not Found -->
|
|
<Section v-else-if="!supplier" variant="plain" paddingY="lg">
|
|
<Card padding="lg">
|
|
<Stack align="center" gap="4">
|
|
<IconCircle tone="primary">
|
|
<Icon name="lucide:building-2" size="24" />
|
|
</IconCircle>
|
|
<Heading :level="2">{{ t('catalogSupplier.not_found.title') }}</Heading>
|
|
<Text tone="muted">{{ t('catalogSupplier.not_found.subtitle') }}</Text>
|
|
<Button @click="navigateTo(localePath('/catalog'))">
|
|
{{ t('catalogSupplier.actions.back_to_catalog') }}
|
|
</Button>
|
|
</Stack>
|
|
</Card>
|
|
</Section>
|
|
|
|
<template v-else>
|
|
<!-- Map Hero -->
|
|
<MapHero
|
|
:title="supplier.name"
|
|
:location="supplierLocation"
|
|
:badges="supplierBadges"
|
|
:initial-zoom="3"
|
|
/>
|
|
|
|
<!-- Offers Section -->
|
|
<Section v-if="offers.length > 0" variant="plain" paddingY="md">
|
|
<Stack gap="4">
|
|
<Heading :level="2">{{ t('catalogSupplier.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>
|
|
|
|
<!-- Products Section -->
|
|
<Section v-if="uniqueProducts.length > 0" variant="plain" paddingY="md">
|
|
<Stack gap="4">
|
|
<Heading :level="2">{{ t('catalogSupplier.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>
|
|
|
|
<!-- Locations Map Section -->
|
|
<Section v-if="uniqueLocations.length > 0" variant="plain" paddingY="md">
|
|
<Stack gap="4">
|
|
<Heading :level="2">{{ t('catalogSupplier.sections.locations.title') }}</Heading>
|
|
<div class="h-64 rounded-lg overflow-hidden border border-base-300 bg-base-100">
|
|
<ClientOnly>
|
|
<MapboxGlobe
|
|
:key="`supplier-locations-${supplierId}`"
|
|
:map-id="`supplier-locations-${supplierId}`"
|
|
:locations="mapLocations"
|
|
:height="256"
|
|
:initial-zoom="3"
|
|
/>
|
|
</ClientOnly>
|
|
</div>
|
|
<Stack direction="row" gap="2" wrap>
|
|
<NuxtLink
|
|
v-for="location in uniqueLocations"
|
|
:key="location.uuid"
|
|
:to="localePath(`/catalog/hubs/${location.uuid}`)"
|
|
>
|
|
<Pill variant="outline" class="hover:bg-base-200 transition-colors cursor-pointer">
|
|
<Icon name="lucide:map-pin" size="14" />
|
|
{{ location.name }}
|
|
</Pill>
|
|
</NuxtLink>
|
|
</Stack>
|
|
</Stack>
|
|
</Section>
|
|
|
|
</template>
|
|
</Stack>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
GetSupplierProfileDocument,
|
|
GetSupplierOffersDocument,
|
|
GetSupplierProfilesDocument,
|
|
GetOffersDocument,
|
|
} from '~/composables/graphql/public/exchange-generated'
|
|
|
|
definePageMeta({
|
|
layout: 'topnav'
|
|
})
|
|
|
|
const route = useRoute()
|
|
const localePath = useLocalePath()
|
|
const { t } = useI18n()
|
|
|
|
const isLoading = ref(true)
|
|
const supplier = ref<any>(null)
|
|
const offers = ref<any[]>([])
|
|
|
|
const supplierId = computed(() => route.params.id as string)
|
|
|
|
// Supplier location for map - use supplier's own coordinates
|
|
const supplierLocation = computed(() => {
|
|
if (supplier.value?.latitude && supplier.value?.longitude) {
|
|
return {
|
|
uuid: supplier.value.uuid,
|
|
name: supplier.value.name,
|
|
latitude: supplier.value.latitude,
|
|
longitude: supplier.value.longitude,
|
|
country: supplier.value.country,
|
|
countryCode: supplier.value.countryCode
|
|
}
|
|
}
|
|
// Fallback to first offer location if supplier has no coordinates
|
|
const firstOffer = offers.value.find(o => o.locationLatitude && o.locationLongitude)
|
|
if (firstOffer) {
|
|
return {
|
|
uuid: firstOffer.locationUuid,
|
|
name: firstOffer.locationName,
|
|
latitude: firstOffer.locationLatitude,
|
|
longitude: firstOffer.locationLongitude,
|
|
country: firstOffer.locationCountry,
|
|
countryCode: firstOffer.locationCountryCode
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
|
|
// Badges for MapHero
|
|
const supplierBadges = computed(() => {
|
|
const badges: Array<{ icon?: string; text: string }> = []
|
|
if (supplier.value?.country) {
|
|
badges.push({ icon: 'lucide:globe', text: supplier.value.country })
|
|
}
|
|
if (supplier.value?.isVerified) {
|
|
badges.push({ icon: 'lucide:check-circle', text: t('catalogSupplier.badges.verified') })
|
|
}
|
|
if (offers.value.length > 0) {
|
|
badges.push({ icon: 'lucide:package', text: t('catalogSupplier.badges.offers', { count: offers.value.length }) })
|
|
}
|
|
return badges
|
|
})
|
|
|
|
// Unique products from offers
|
|
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())
|
|
})
|
|
|
|
// Unique locations from offers
|
|
const uniqueLocations = computed(() => {
|
|
const locations = new Map<string, { uuid: string; name: string; latitude: number; longitude: number; country?: string | null; countryCode?: string | null }>()
|
|
offers.value.forEach(offer => {
|
|
if (offer.locationUuid && offer.locationName && offer.locationLatitude && offer.locationLongitude) {
|
|
locations.set(offer.locationUuid, {
|
|
uuid: offer.locationUuid,
|
|
name: offer.locationName,
|
|
latitude: offer.locationLatitude,
|
|
longitude: offer.locationLongitude,
|
|
country: offer.locationCountry,
|
|
countryCode: offer.locationCountryCode
|
|
})
|
|
}
|
|
})
|
|
return Array.from(locations.values())
|
|
})
|
|
|
|
// Locations for the map
|
|
const mapLocations = computed(() => {
|
|
return uniqueLocations.value.map(loc => ({
|
|
uuid: loc.uuid,
|
|
name: loc.name,
|
|
latitude: loc.latitude,
|
|
longitude: loc.longitude,
|
|
country: ''
|
|
}))
|
|
})
|
|
|
|
try {
|
|
const { data: supplierData } = await useServerQuery('catalog-supplier-detail', GetSupplierProfileDocument, { uuid: supplierId.value }, 'public', 'exchange')
|
|
supplier.value = supplierData.value?.getSupplierProfile || null
|
|
|
|
if (!supplier.value) {
|
|
const { data: suppliersList } = await useServerQuery('catalog-suppliers-fallback', GetSupplierProfilesDocument, {}, 'public', 'exchange')
|
|
supplier.value = (suppliersList.value?.getSupplierProfiles || []).find((s: any) => s?.teamUuid === supplierId.value || s?.uuid === supplierId.value) || null
|
|
}
|
|
|
|
const teamIds = [
|
|
supplier.value?.teamUuid,
|
|
supplier.value?.uuid,
|
|
supplierId.value
|
|
].filter(Boolean)
|
|
|
|
if (teamIds.length) {
|
|
const primaryId = teamIds[0] as string
|
|
const { data: offersData } = await useServerQuery('catalog-supplier-offers', GetSupplierOffersDocument, { teamUuid: primaryId }, 'public', 'exchange')
|
|
offers.value = offersData.value?.getOffers || []
|
|
|
|
if (!offers.value.length) {
|
|
const { data: allOffersData } = await useServerQuery('catalog-supplier-offers-fallback', GetOffersDocument, {}, 'public', 'exchange')
|
|
const ids = new Set(teamIds)
|
|
offers.value = (allOffersData.value?.getOffers || []).filter((o: any) => o?.teamUuid && ids.has(o.teamUuid))
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading supplier:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
|
|
// SEO
|
|
useHead(() => ({
|
|
title: supplier.value?.name
|
|
? t('catalogSupplier.meta.title_with_name', { name: supplier.value.name })
|
|
: t('catalogSupplier.meta.title'),
|
|
meta: [
|
|
{
|
|
name: 'description',
|
|
content: supplier.value?.description
|
|
|| (supplier.value?.name
|
|
? t('catalogSupplier.meta.description_with_name', { name: supplier.value.name })
|
|
: t('catalogSupplier.meta.description'))
|
|
}
|
|
]
|
|
}))
|
|
</script>
|