Files
webapp/app/pages/clientarea/addresses/index.vue
Ruslan Bakiev 7ea96a97b3
All checks were successful
Build Docker Image / build (push) Successful in 4m1s
Refactor catalog layout: replace Teleport with provide/inject
- Create useCatalogLayout composable for data transfer from pages to layout
- Layout now owns SearchBar and CatalogMap components directly
- Pages provide data via provideCatalogLayout()
- Fixes navigation glitches (multiple SearchBars) when switching tabs
- Support custom subNavItems for clientarea pages
- Unify 6 pages to use catalog layout:
  - catalog/offers, suppliers, hubs
  - clientarea/orders, addresses, offers
2026-01-15 10:49:40 +07:00

174 lines
4.9 KiB
Vue

<template>
<div>
<!-- Add button -->
<div class="mb-4">
<NuxtLink :to="localePath('/clientarea/addresses/new')">
<Button variant="outline" class="w-full">
<Icon name="lucide:plus" size="16" class="mr-2" />
{{ t('profileAddresses.actions.add') }}
</Button>
</NuxtLink>
</div>
<!-- List content -->
<div v-if="isLoading" class="flex items-center justify-center py-8">
<Card padding="lg">
<Stack align="center" justify="center" gap="3">
<Spinner />
<Text tone="muted">{{ t('catalogLanding.states.loading') }}</Text>
</Stack>
</Card>
</div>
<template v-else>
<Stack gap="3">
<div
v-for="item in displayItems"
:key="item.uuid"
:class="{ 'ring-2 ring-primary rounded-lg': item.uuid === selectedAddressId }"
@click="onSelectAddress(item)"
@mouseenter="hoveredAddressId = item.uuid"
@mouseleave="hoveredAddressId = undefined"
>
<NuxtLink :to="localePath(`/clientarea/addresses/${item.uuid}`)" class="block">
<Card padding="sm" interactive>
<div class="flex flex-col gap-1">
<Text size="base" weight="semibold" class="truncate">{{ item.name }}</Text>
<Text tone="muted" size="sm" class="line-clamp-2">{{ item.address }}</Text>
<div class="flex items-center mt-1">
<span class="text-lg">{{ isoToEmoji(item.countryCode) }}</span>
</div>
</div>
</Card>
</NuxtLink>
</div>
</Stack>
<Stack v-if="displayItems.length === 0" align="center" gap="2" class="py-8">
<EmptyState
icon="📍"
:title="t('profileAddresses.empty.title')"
:description="t('profileAddresses.empty.description')"
:action-label="t('profileAddresses.empty.cta')"
:action-to="localePath('/clientarea/addresses/new')"
action-icon="lucide:plus"
/>
</Stack>
</template>
</div>
</template>
<script setup lang="ts">
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
import { provideCatalogLayout } from '~/composables/useCatalogLayout'
definePageMeta({
layout: 'catalog',
middleware: ['auth-oidc']
})
const { t } = useI18n()
const localePath = useLocalePath()
const {
items,
isLoading,
isoToEmoji,
init
} = useTeamAddresses()
const selectedAddressId = ref<string>()
const hoveredAddressId = ref<string>()
// Search bar
const searchQuery = ref('')
// Search with map checkbox
const searchWithMap = ref(false)
const currentBounds = ref<MapBounds | null>(null)
const onBoundsChange = (bounds: MapBounds) => {
currentBounds.value = bounds
}
// Map items
const itemsWithCoords = computed(() => {
return items.value.filter(addr =>
addr.latitude != null &&
addr.longitude != null &&
!isNaN(Number(addr.latitude)) &&
!isNaN(Number(addr.longitude))
).map(addr => ({
uuid: addr.uuid,
name: addr.name,
latitude: Number(addr.latitude),
longitude: Number(addr.longitude)
}))
})
// Filtered items when searchWithMap is enabled
const displayItems = computed(() => {
if (!searchWithMap.value || !currentBounds.value) return items.value
return items.value.filter(item => {
if (item.latitude == null || item.longitude == null) return false
const { west, east, north, south } = currentBounds.value!
const lng = Number(item.longitude)
const lat = Number(item.latitude)
return lng >= west && lng <= east && lat >= south && lat <= north
})
})
// Hovered item with coordinates for map highlight
const hoveredItem = computed(() => {
if (!hoveredAddressId.value) return null
const item = items.value.find(i => i.uuid === hoveredAddressId.value)
if (!item?.latitude || !item?.longitude) return null
return { latitude: Number(item.latitude), longitude: Number(item.longitude) }
})
// Search handler
const onSearch = () => {
// TODO: Implement search
}
// No filters
const onRemoveFilter = (_id: string) => {}
const onSelectAddress = (item: any) => {
selectedAddressId.value = item.uuid
}
// Handle selection from map
const onMapSelect = (uuid: string) => {
const address = items.value.find(i => i.uuid === uuid)
if (address) {
selectedAddressId.value = uuid
}
}
// Provide data to layout
provideCatalogLayout({
// Custom subnav for clientarea
subNavItems: [
{ label: t('cabinetNav.orders'), path: '/clientarea/orders' },
{ label: t('cabinetNav.addresses'), path: '/clientarea/addresses' },
],
searchQuery,
activeFilters: ref([]),
displayedCount: computed(() => displayItems.value.length),
totalCount: computed(() => items.value.length),
onSearch,
onRemoveFilter,
mapItems: itemsWithCoords,
mapId: 'addresses-map',
pointColor: '#10b981',
hoveredItemId: hoveredAddressId,
hoveredItem,
onMapSelect,
onBoundsChange,
searchWithMap
})
await init()
</script>