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:
@@ -112,18 +112,18 @@ const handleSearch = () => {
|
||||
query.quantity = String(quantity.value)
|
||||
}
|
||||
|
||||
// Navigate to offers flow
|
||||
// Navigate to unified catalog
|
||||
if (productUuid.value && locationUuid.value) {
|
||||
// Both product and hub selected -> show offers
|
||||
router.push({
|
||||
path: localePath(`/catalog/offers/${productUuid.value}/${locationUuid.value}`),
|
||||
query
|
||||
path: localePath('/catalog'),
|
||||
query: { product: productUuid.value, hub: locationUuid.value, ...query }
|
||||
})
|
||||
} else if (productUuid.value) {
|
||||
// Only product selected -> select hub
|
||||
// Only product selected -> show hubs for product
|
||||
router.push({
|
||||
path: localePath(`/catalog/offers/${productUuid.value}`),
|
||||
query
|
||||
path: localePath('/catalog'),
|
||||
query: { product: productUuid.value, ...query }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
133
app/components/search/UnifiedSearchBar.vue
Normal file
133
app/components/search/UnifiedSearchBar.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="bg-base-100 rounded-box shadow-md">
|
||||
<!-- Search bar row -->
|
||||
<div class="flex items-center gap-2 p-3 flex-wrap">
|
||||
<!-- Active filter tokens -->
|
||||
<div
|
||||
v-for="token in activeTokens"
|
||||
:key="token.type"
|
||||
class="badge badge-lg gap-1 cursor-pointer hover:badge-primary transition-colors"
|
||||
@click="onEditToken(token.type)"
|
||||
>
|
||||
<Icon :name="token.icon" size="14" />
|
||||
<span class="max-w-32 truncate">{{ token.label }}</span>
|
||||
<button
|
||||
class="ml-1 hover:text-error"
|
||||
@click.stop="onRemoveToken(token.type)"
|
||||
>
|
||||
<Icon name="lucide:x" size="12" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Active selection mode indicator -->
|
||||
<div
|
||||
v-if="selectMode"
|
||||
class="badge badge-lg badge-outline badge-primary gap-1"
|
||||
>
|
||||
<Icon :name="selectModeIcon" size="14" />
|
||||
{{ selectModeLabel }}: ?
|
||||
<button
|
||||
class="ml-1 hover:text-error"
|
||||
@click="onCancelSelect"
|
||||
>
|
||||
<Icon name="lucide:x" size="12" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search input -->
|
||||
<div class="flex-1 min-w-48">
|
||||
<input
|
||||
v-model="localSearchQuery"
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
class="input input-bordered w-full"
|
||||
@input="onSearchInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick filter chips -->
|
||||
<div
|
||||
v-if="availableChips.length > 0"
|
||||
class="flex items-center gap-2 px-3 pb-3 flex-wrap"
|
||||
>
|
||||
<button
|
||||
v-for="chip in availableChips"
|
||||
:key="chip.type"
|
||||
class="btn btn-sm btn-ghost gap-1"
|
||||
@click="onStartSelect(chip.type)"
|
||||
>
|
||||
<Icon name="lucide:plus" size="14" />
|
||||
{{ chip.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectMode } from '~/composables/useCatalogSearch'
|
||||
|
||||
const props = defineProps<{
|
||||
activeTokens: Array<{ type: string; id: string; label: string; icon: string }>
|
||||
availableChips: Array<{ type: string; label: string }>
|
||||
selectMode: SelectMode
|
||||
searchQuery: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'start-select', type: string): void
|
||||
(e: 'cancel-select'): void
|
||||
(e: 'edit-token', type: string): void
|
||||
(e: 'remove-token', type: string): void
|
||||
(e: 'update:search-query', value: string): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const localSearchQuery = ref(props.searchQuery)
|
||||
|
||||
watch(() => props.searchQuery, (val) => {
|
||||
localSearchQuery.value = val
|
||||
})
|
||||
|
||||
const placeholder = computed(() => {
|
||||
if (props.selectMode === 'product') return t('catalog.search.searchProducts')
|
||||
if (props.selectMode === 'supplier') return t('catalog.search.searchSuppliers')
|
||||
if (props.selectMode === 'hub') return t('catalog.search.searchHubs')
|
||||
return t('catalog.search.placeholder')
|
||||
})
|
||||
|
||||
const selectModeLabel = computed(() => {
|
||||
if (props.selectMode === 'product') return t('catalog.filters.product')
|
||||
if (props.selectMode === 'supplier') return t('catalog.filters.supplier')
|
||||
if (props.selectMode === 'hub') return t('catalog.filters.hub')
|
||||
return ''
|
||||
})
|
||||
|
||||
const selectModeIcon = computed(() => {
|
||||
if (props.selectMode === 'product') return 'lucide:package'
|
||||
if (props.selectMode === 'supplier') return 'lucide:factory'
|
||||
if (props.selectMode === 'hub') return 'lucide:map-pin'
|
||||
return 'lucide:search'
|
||||
})
|
||||
|
||||
const onStartSelect = (type: string) => {
|
||||
emit('start-select', type)
|
||||
}
|
||||
|
||||
const onCancelSelect = () => {
|
||||
emit('cancel-select')
|
||||
}
|
||||
|
||||
const onEditToken = (type: string) => {
|
||||
emit('edit-token', type)
|
||||
}
|
||||
|
||||
const onRemoveToken = (type: string) => {
|
||||
emit('remove-token', type)
|
||||
}
|
||||
|
||||
const onSearchInput = () => {
|
||||
emit('update:search-query', localSearchQuery.value)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user