Implement unified catalog search with token-based filtering
All checks were successful
Build Docker Image / build (push) Successful in 3m23s
All checks were successful
Build Docker Image / build (push) Successful in 3m23s
- Add useCatalogSearch composable for managing unified search state - Add UnifiedSearchBar component with token chips for filters - Add CatalogHero component for empty/landing state - Create grid components for each display mode: - CatalogGridProducts, CatalogGridSuppliers, CatalogGridHubs - CatalogGridHubsForProduct, CatalogGridProductsFromSupplier - CatalogGridProductsInHub, CatalogGridOffers - Add unified catalog page at /catalog with query params - Remove SubNavigation from catalog section (kept for other sections) - Update all links to use new unified catalog paths - Delete old nested catalog pages (offers/suppliers/hubs flows) - Add i18n translations for catalog section
This commit is contained in:
124
app/components/catalog/CatalogGridOffers.vue
Normal file
124
app/components/catalog/CatalogGridOffers.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div>
|
||||
<Text size="lg" weight="semibold" class="mb-4">
|
||||
{{ t('catalog.headers.offers') }}
|
||||
<span v-if="!isLoading" class="text-base-content/50 font-normal">
|
||||
({{ filteredItems.length }})
|
||||
</span>
|
||||
</Text>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredItems.length === 0" class="text-center py-12">
|
||||
<Text tone="muted">{{ t('catalog.empty.noOffers') }}</Text>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<OfferCard
|
||||
v-for="offer in filteredItems"
|
||||
:key="offer.uuid"
|
||||
:offer="offer"
|
||||
linkable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
GetOffersDocument,
|
||||
GetSupplierOffersDocument
|
||||
} from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
const props = defineProps<{
|
||||
productId?: string | null
|
||||
supplierId?: string | null
|
||||
hubId?: string | null
|
||||
searchQuery: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { execute } = useGraphQL()
|
||||
|
||||
const items = ref<any[]>([])
|
||||
const isLoading = ref(true)
|
||||
|
||||
const fetchData = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
let offers: any[] = []
|
||||
|
||||
if (props.productId && props.hubId) {
|
||||
// Product + Hub = specific offers
|
||||
const data = await execute(
|
||||
GetOffersDocument,
|
||||
{
|
||||
productUuid: props.productId,
|
||||
locationUuid: props.hubId
|
||||
},
|
||||
'public',
|
||||
'exchange'
|
||||
)
|
||||
offers = data?.getOffers || []
|
||||
} else if (props.supplierId && props.productId) {
|
||||
// Supplier + Product = offers from supplier for product
|
||||
const data = await execute(
|
||||
GetSupplierOffersDocument,
|
||||
{ teamUuid: props.supplierId },
|
||||
'public',
|
||||
'exchange'
|
||||
)
|
||||
offers = (data?.getOffers || []).filter(
|
||||
(o: any) => o.productUuid === props.productId
|
||||
)
|
||||
} else if (props.supplierId) {
|
||||
// Just supplier = all offers from supplier
|
||||
const data = await execute(
|
||||
GetSupplierOffersDocument,
|
||||
{ teamUuid: props.supplierId },
|
||||
'public',
|
||||
'exchange'
|
||||
)
|
||||
offers = data?.getOffers || []
|
||||
} else if (props.hubId) {
|
||||
// Just hub = all offers in hub
|
||||
const data = await execute(
|
||||
GetOffersDocument,
|
||||
{ locationUuid: props.hubId },
|
||||
'public',
|
||||
'exchange'
|
||||
)
|
||||
offers = data?.getOffers || []
|
||||
} else if (props.productId) {
|
||||
// Just product = all offers for product
|
||||
const data = await execute(
|
||||
GetOffersDocument,
|
||||
{ productUuid: props.productId },
|
||||
'public',
|
||||
'exchange'
|
||||
)
|
||||
offers = data?.getOffers || []
|
||||
}
|
||||
|
||||
items.value = offers
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
|
||||
watch([() => props.productId, () => props.supplierId, () => props.hubId], fetchData)
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!props.searchQuery.trim()) return items.value
|
||||
const q = props.searchQuery.toLowerCase()
|
||||
return items.value.filter((item: any) =>
|
||||
item.productName?.toLowerCase().includes(q) ||
|
||||
item.teamName?.toLowerCase().includes(q) ||
|
||||
item.locationName?.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user