Add Airbnb-style "search as I move" checkbox + hover highlight
All checks were successful
Build Docker Image / build (push) Successful in 3m33s

- Move filter checkbox to right side, same line as view toggle
- Add hover events on selection cards to highlight map points
- Update translations: "Искать при перемещении" / "Search as I move the map"
This commit is contained in:
Ruslan Bakiev
2026-01-24 11:07:31 +07:00
parent d03564a2d9
commit 2fc4dfb834
5 changed files with 77 additions and 40 deletions

View File

@@ -14,17 +14,6 @@
:placeholder="searchPlaceholder" :placeholder="searchPlaceholder"
class="input input-sm w-full bg-white/50 border-base-300/50 text-base-content placeholder:text-base-content/50" class="input input-sm w-full bg-white/50 border-base-300/50 text-base-content placeholder:text-base-content/50"
/> />
<!-- Filter by map bounds checkbox -->
<label class="flex items-center gap-2 mt-2 text-sm cursor-pointer text-base-content/80 hover:text-base-content">
<input
type="checkbox"
:checked="filterByBounds"
class="checkbox checkbox-sm checkbox-primary"
@change="emit('update:filter-by-bounds', ($event.target as HTMLInputElement).checked)"
/>
<Icon name="lucide:map" size="14" />
<span>{{ $t('catalog.search.filterByMap') }}</span>
</label>
</div> </div>
<!-- List --> <!-- List -->
@@ -41,39 +30,54 @@
<div v-else class="flex flex-col gap-2"> <div v-else class="flex flex-col gap-2">
<!-- Products --> <!-- Products -->
<template v-if="selectMode === 'product'"> <template v-if="selectMode === 'product'">
<ProductCard <div
v-for="item in filteredItems" v-for="item in filteredItems"
:key="item.uuid" :key="item.uuid"
@mouseenter="emit('hover', item.uuid)"
@mouseleave="emit('hover', null)"
>
<ProductCard
:product="item" :product="item"
selectable selectable
compact compact
:is-selected="selectedId === item.uuid" :is-selected="selectedId === item.uuid"
@select="onSelect(item)" @select="onSelect(item)"
/> />
</div>
</template> </template>
<!-- Hubs --> <!-- Hubs -->
<template v-else-if="selectMode === 'hub'"> <template v-else-if="selectMode === 'hub'">
<HubCard <div
v-for="item in filteredItems" v-for="item in filteredItems"
:key="item.uuid" :key="item.uuid"
@mouseenter="emit('hover', item.uuid)"
@mouseleave="emit('hover', null)"
>
<HubCard
:hub="item" :hub="item"
selectable selectable
:is-selected="selectedId === item.uuid" :is-selected="selectedId === item.uuid"
@select="onSelect(item)" @select="onSelect(item)"
/> />
</div>
</template> </template>
<!-- Suppliers --> <!-- Suppliers -->
<template v-else-if="selectMode === 'supplier'"> <template v-else-if="selectMode === 'supplier'">
<SupplierCard <div
v-for="item in filteredItems" v-for="item in filteredItems"
:key="item.uuid" :key="item.uuid"
@mouseenter="emit('hover', item.uuid)"
@mouseleave="emit('hover', null)"
>
<SupplierCard
:supplier="item" :supplier="item"
selectable selectable
:is-selected="selectedId === item.uuid" :is-selected="selectedId === item.uuid"
@select="onSelect(item)" @select="onSelect(item)"
/> />
</div>
</template> </template>
<!-- Infinite scroll sentinel --> <!-- Infinite scroll sentinel -->
@@ -107,14 +111,13 @@ const props = defineProps<{
loading?: boolean loading?: boolean
loadingMore?: boolean loadingMore?: boolean
hasMore?: boolean hasMore?: boolean
filterByBounds?: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
'select': [type: string, item: Item] 'select': [type: string, item: Item]
'close': [] 'close': []
'load-more': [] 'load-more': []
'update:filter-by-bounds': [value: boolean] 'hover': [uuid: string | null]
}>() }>()
const { t } = useI18n() const { t } = useI18n()

View File

@@ -38,8 +38,23 @@
<span class="text-white text-sm">{{ $t('common.loading') }}</span> <span class="text-white text-sm">{{ $t('common.loading') }}</span>
</div> </div>
<!-- View toggle (top RIGHT overlay, below header) - works in both modes --> <!-- View toggle + filter checkbox (top RIGHT overlay, below header) -->
<div class="absolute top-[116px] right-4 z-20 hidden lg:block"> <div class="absolute top-[116px] right-4 z-20 hidden lg:flex items-center gap-2">
<!-- Filter by bounds checkbox (when panel is open) -->
<label
v-if="showPanel"
class="flex items-center gap-2 bg-black/30 backdrop-blur-md rounded-lg px-3 py-1.5 border border-white/10 cursor-pointer text-white text-sm hover:bg-black/40 transition-colors"
>
<input
type="checkbox"
:checked="filterByBounds"
class="checkbox checkbox-xs checkbox-primary"
@change="$emit('update:filter-by-bounds', ($event.target as HTMLInputElement).checked)"
/>
<span>{{ $t('catalog.search.filterByMap') }}</span>
</label>
<!-- View mode toggle -->
<div class="flex gap-1 bg-black/30 backdrop-blur-md rounded-lg p-1 border border-white/10"> <div class="flex gap-1 bg-black/30 backdrop-blur-md rounded-lg p-1 border border-white/10">
<button <button
class="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors" class="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors"
@@ -194,6 +209,7 @@ const props = withDefaults(defineProps<{
hoveredId?: string hoveredId?: string
items?: MapItem[] items?: MapItem[]
showPanel?: boolean showPanel?: boolean
filterByBounds?: boolean
}>(), { }>(), {
loading: false, loading: false,
useServerClustering: true, useServerClustering: true,
@@ -201,13 +217,15 @@ const props = withDefaults(defineProps<{
mapId: 'catalog-map', mapId: 'catalog-map',
pointColor: '#f97316', pointColor: '#f97316',
items: () => [], items: () => [],
showPanel: false showPanel: false,
filterByBounds: false
}) })
const emit = defineEmits<{ const emit = defineEmits<{
'select': [item: MapItem] 'select': [item: MapItem]
'bounds-change': [bounds: MapBounds] 'bounds-change': [bounds: MapBounds]
'update:hoveredId': [uuid: string | undefined] 'update:hoveredId': [uuid: string | undefined]
'update:filter-by-bounds': [value: boolean]
}>() }>()
// Server-side clustering - use computed node type based on view mode // Server-side clustering - use computed node type based on view mode

View File

@@ -6,10 +6,13 @@
:cluster-node-type="clusterNodeType" :cluster-node-type="clusterNodeType"
map-id="unified-catalog-map" map-id="unified-catalog-map"
:point-color="mapPointColor" :point-color="mapPointColor"
:items="[]" :items="currentSelectionItems"
:hovered-id="hoveredItemId"
:show-panel="showPanel" :show-panel="showPanel"
:filter-by-bounds="filterByBounds"
@select="onMapSelect" @select="onMapSelect"
@bounds-change="onBoundsChange" @bounds-change="onBoundsChange"
@update:filter-by-bounds="filterByBounds = $event"
> >
<!-- Panel slot - shows selection list OR quote results --> <!-- Panel slot - shows selection list OR quote results -->
<template #panel> <template #panel>
@@ -23,11 +26,10 @@
:loading="selectionLoading" :loading="selectionLoading"
:loading-more="selectionLoadingMore" :loading-more="selectionLoadingMore"
:has-more="selectionHasMore && !filterByBounds" :has-more="selectionHasMore && !filterByBounds"
:filter-by-bounds="filterByBounds"
@select="onSelectItem" @select="onSelectItem"
@close="cancelSelect" @close="cancelSelect"
@load-more="onLoadMore" @load-more="onLoadMore"
@update:filter-by-bounds="filterByBounds = $event" @hover="onHoverItem"
/> />
<!-- Quote results: show offers after search --> <!-- Quote results: show offers after search -->
@@ -61,6 +63,20 @@ const catalogPageRef = ref<{ currentBounds: Ref<MapBounds | null> } | null>(null
const filterByBounds = ref(false) const filterByBounds = ref(false)
const currentMapBounds = ref<MapBounds | null>(null) const currentMapBounds = ref<MapBounds | null>(null)
// Hovered item for map highlight
const hoveredItemId = ref<string | null>(null)
const onHoverItem = (uuid: string | null) => {
hoveredItemId.value = uuid
}
// Current selection items for hover highlighting on map
const currentSelectionItems = computed(() => {
if (selectMode.value === 'product') return filteredProducts.value
if (selectMode.value === 'hub') return filteredHubs.value
if (selectMode.value === 'supplier') return filteredSuppliers.value
return []
})
// Handle bounds change from map // Handle bounds change from map
const onBoundsChange = (bounds: MapBounds) => { const onBoundsChange = (bounds: MapBounds) => {
currentMapBounds.value = bounds currentMapBounds.value = bounds

View File

@@ -12,7 +12,7 @@
"searchProducts": "Search products...", "searchProducts": "Search products...",
"searchSuppliers": "Search suppliers...", "searchSuppliers": "Search suppliers...",
"searchHubs": "Search hubs...", "searchHubs": "Search hubs...",
"filterByMap": "In map area" "filterByMap": "Search as I move the map"
}, },
"hero": { "hero": {
"title": "Find the best offer", "title": "Find the best offer",

View File

@@ -12,7 +12,7 @@
"searchProducts": "Поиск товаров...", "searchProducts": "Поиск товаров...",
"searchSuppliers": "Поиск поставщиков...", "searchSuppliers": "Поиск поставщиков...",
"searchHubs": "Поиск хабов...", "searchHubs": "Поиск хабов...",
"filterByMap": "В области карты" "filterByMap": "Искать при перемещении"
}, },
"hero": { "hero": {
"title": "Найдите лучшее предложение", "title": "Найдите лучшее предложение",