Add MarketFuzzySearch component for enhanced product searching
- Introduced a new MarketFuzzySearch component to provide an advanced search interface with keyboard shortcuts, search suggestions, and recent searches functionality. - Updated MarketPage and StallView to integrate the new fuzzy search component, replacing the previous search input implementations. - Enhanced search capabilities with configurable options for better user experience and product discovery. These changes improve the search functionality across the market module, making it easier for users to find products efficiently.
This commit is contained in:
parent
86d3133978
commit
8aa575ffb1
3 changed files with 470 additions and 34 deletions
359
src/modules/market/components/MarketFuzzySearch.vue
Normal file
359
src/modules/market/components/MarketFuzzySearch.vue
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
<template>
|
||||
<div class="market-fuzzy-search" :class="props.class">
|
||||
<!-- Enhanced Search Input with Keyboard Shortcuts -->
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search class="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<Input
|
||||
ref="searchInputRef"
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="handleSearchChange"
|
||||
@keydown="handleKeydown"
|
||||
:placeholder="enhancedPlaceholder"
|
||||
:disabled="disabled"
|
||||
class="pl-10 pr-20"
|
||||
/>
|
||||
|
||||
<!-- Keyboard Shortcuts Hint -->
|
||||
<div class="absolute inset-y-0 right-0 pr-3 flex items-center gap-2">
|
||||
<div v-if="showKeyboardHints && !searchQuery" class="text-xs text-muted-foreground hidden sm:flex items-center gap-1">
|
||||
<Badge variant="outline" class="px-1 py-0 text-xs">⌘ K</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Clear Button -->
|
||||
<Button
|
||||
v-if="searchQuery"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="handleClear"
|
||||
class="h-6 w-6 p-0 hover:bg-muted"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
<span class="sr-only">Clear search</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Search Results with Categories and Previews -->
|
||||
<div v-if="isSearching && showResultCount" class="mt-2 flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>{{ resultCount }} result{{ resultCount === 1 ? '' : 's' }} found</span>
|
||||
|
||||
<!-- Quick Filters -->
|
||||
<div v-if="searchQuery && topCategories.length > 0" class="flex items-center gap-1">
|
||||
<span class="text-xs">In:</span>
|
||||
<Button
|
||||
v-for="category in topCategories.slice(0, 3)"
|
||||
:key="category"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="filterByCategory(category)"
|
||||
class="h-5 px-1 text-xs"
|
||||
>
|
||||
{{ category }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Suggestions (when no input but focused) -->
|
||||
<div v-if="showSuggestions && !searchQuery && isFocused" class="mt-2 p-3 border rounded-lg bg-muted/50">
|
||||
<div class="text-sm font-medium mb-2">Try searching for:</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Button
|
||||
v-for="suggestion in searchSuggestions"
|
||||
:key="suggestion"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="applySuggestion(suggestion)"
|
||||
class="h-6 px-2 text-xs"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Searches -->
|
||||
<div v-if="showRecentSearches && recentSearches.length > 0 && !searchQuery && isFocused" class="mt-2 p-3 border rounded-lg bg-muted/50">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-sm font-medium">Recent searches:</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="clearRecentSearches"
|
||||
class="h-5 px-1 text-xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Button
|
||||
v-for="recent in recentSearches.slice(0, 5)"
|
||||
:key="recent"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="applyRecentSearch(recent)"
|
||||
class="h-6 px-2 text-xs"
|
||||
>
|
||||
<History class="w-3 h-3 mr-1" />
|
||||
{{ recent }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useFuzzySearch, type FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Search, X, History } from 'lucide-vue-next'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import type { Product } from '../types/market'
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The data to search through
|
||||
*/
|
||||
data: Product[]
|
||||
/**
|
||||
* Configuration options for the fuzzy search
|
||||
*/
|
||||
options?: FuzzySearchOptions<Product>
|
||||
/**
|
||||
* Placeholder text for the search input
|
||||
*/
|
||||
placeholder?: string
|
||||
/**
|
||||
* Whether to show keyboard hints
|
||||
*/
|
||||
showKeyboardHints?: boolean
|
||||
/**
|
||||
* Whether to show the result count
|
||||
*/
|
||||
showResultCount?: boolean
|
||||
/**
|
||||
* Whether to show search suggestions
|
||||
*/
|
||||
showSuggestions?: boolean
|
||||
/**
|
||||
* Whether to show recent searches
|
||||
*/
|
||||
showRecentSearches?: boolean
|
||||
/**
|
||||
* Custom class for the search container
|
||||
*/
|
||||
class?: string | string[]
|
||||
/**
|
||||
* Whether the search input should be disabled
|
||||
*/
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'search', query: string): void
|
||||
(e: 'results', results: Product[]): void
|
||||
(e: 'clear'): void
|
||||
(e: 'filter-category', category: string): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: 'Search products, stalls, categories...',
|
||||
showKeyboardHints: true,
|
||||
showResultCount: true,
|
||||
showSuggestions: true,
|
||||
showRecentSearches: true,
|
||||
disabled: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Create reactive data ref for the composable
|
||||
const dataRef = computed(() => props.data)
|
||||
|
||||
// Use the fuzzy search composable
|
||||
const {
|
||||
searchQuery,
|
||||
filteredItems,
|
||||
isSearching,
|
||||
resultCount,
|
||||
clearSearch,
|
||||
setSearchQuery
|
||||
} = useFuzzySearch(dataRef, props.options)
|
||||
|
||||
// Local state
|
||||
const searchInputRef = ref<HTMLInputElement>()
|
||||
const isFocused = ref(false)
|
||||
|
||||
// Persistent recent searches (stored in localStorage)
|
||||
const recentSearches = useLocalStorage<string[]>('market-recent-searches', [])
|
||||
|
||||
// Enhanced placeholder with keyboard shortcut
|
||||
const enhancedPlaceholder = computed(() => {
|
||||
if (props.showKeyboardHints) {
|
||||
return `${props.placeholder} (⌘K to focus)`
|
||||
}
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
// Extract categories from search results for quick filters
|
||||
const topCategories = computed(() => {
|
||||
const categoryCount = new Map<string, number>()
|
||||
|
||||
filteredItems.value.forEach(product => {
|
||||
product.categories?.forEach(category => {
|
||||
categoryCount.set(category, (categoryCount.get(category) || 0) + 1)
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(categoryCount.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([category]) => category)
|
||||
})
|
||||
|
||||
// Generate search suggestions based on popular categories and products
|
||||
const searchSuggestions = computed(() => {
|
||||
const suggestions = new Set<string>()
|
||||
|
||||
// Add popular categories
|
||||
const allCategories = new Map<string, number>()
|
||||
props.data.forEach(product => {
|
||||
product.categories?.forEach(category => {
|
||||
allCategories.set(category, (allCategories.get(category) || 0) + 1)
|
||||
})
|
||||
})
|
||||
|
||||
Array.from(allCategories.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 4)
|
||||
.forEach(([category]) => suggestions.add(category))
|
||||
|
||||
// Add popular stall names
|
||||
const stallNames = new Map<string, number>()
|
||||
props.data.forEach(product => {
|
||||
if (product.stallName && product.stallName !== 'Unknown Stall') {
|
||||
stallNames.set(product.stallName, (stallNames.get(product.stallName) || 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
Array.from(stallNames.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 2)
|
||||
.forEach(([stall]) => suggestions.add(stall))
|
||||
|
||||
return Array.from(suggestions)
|
||||
})
|
||||
|
||||
// Methods
|
||||
const handleSearchChange = (value: string | number) => {
|
||||
const stringValue = String(value)
|
||||
setSearchQuery(stringValue)
|
||||
emit('update:modelValue', stringValue)
|
||||
emit('search', stringValue)
|
||||
emit('results', filteredItems.value)
|
||||
|
||||
// Add to recent searches when user finishes typing
|
||||
if (stringValue.trim() && stringValue.length >= 3) {
|
||||
addToRecentSearches(stringValue.trim())
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
clearSearch()
|
||||
emit('update:modelValue', '')
|
||||
emit('search', '')
|
||||
emit('results', filteredItems.value)
|
||||
emit('clear')
|
||||
searchInputRef.value?.focus()
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
// Escape key clears search
|
||||
if (event.key === 'Escape') {
|
||||
if (searchQuery.value) {
|
||||
handleClear()
|
||||
} else {
|
||||
searchInputRef.value?.blur()
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const filterByCategory = (category: string) => {
|
||||
emit('filter-category', category)
|
||||
setSearchQuery(category)
|
||||
}
|
||||
|
||||
const applySuggestion = (suggestion: string) => {
|
||||
setSearchQuery(suggestion)
|
||||
addToRecentSearches(suggestion)
|
||||
}
|
||||
|
||||
const applyRecentSearch = (recent: string) => {
|
||||
setSearchQuery(recent)
|
||||
}
|
||||
|
||||
const addToRecentSearches = (query: string) => {
|
||||
const searches = recentSearches.value.filter(s => s !== query)
|
||||
searches.unshift(query)
|
||||
recentSearches.value = searches.slice(0, 10) // Keep only 10 recent searches
|
||||
}
|
||||
|
||||
const clearRecentSearches = () => {
|
||||
recentSearches.value = []
|
||||
}
|
||||
|
||||
// Focus handling
|
||||
const handleFocus = () => {
|
||||
isFocused.value = true
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
// Delay hiding to allow clicking on suggestions
|
||||
setTimeout(() => {
|
||||
isFocused.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
const handleGlobalKeydown = (event: KeyboardEvent) => {
|
||||
// ⌘K or Ctrl+K to focus search
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
|
||||
event.preventDefault()
|
||||
searchInputRef.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes in filtered items and emit results
|
||||
watch(filteredItems, (items) => {
|
||||
emit('results', items)
|
||||
}, { immediate: true })
|
||||
|
||||
// Setup and cleanup
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleGlobalKeydown)
|
||||
|
||||
if (searchInputRef.value) {
|
||||
searchInputRef.value.addEventListener('focus', handleFocus)
|
||||
searchInputRef.value.addEventListener('blur', handleBlur)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleGlobalKeydown)
|
||||
|
||||
if (searchInputRef.value) {
|
||||
searchInputRef.value.removeEventListener('focus', handleFocus)
|
||||
searchInputRef.value.removeEventListener('blur', handleBlur)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.market-fuzzy-search {
|
||||
@apply w-full relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -40,12 +40,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<!-- Enhanced Fuzzy Search Bar -->
|
||||
<div class="flex-1 max-w-md ml-8">
|
||||
<Input
|
||||
v-model="marketStore.searchText"
|
||||
type="text"
|
||||
placeholder="Search products..."
|
||||
<MarketFuzzySearch
|
||||
:data="marketStore.products"
|
||||
:options="searchOptions"
|
||||
@results="handleSearchResults"
|
||||
@filter-category="handleCategoryFilter"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -68,15 +69,15 @@
|
|||
</div>
|
||||
|
||||
<!-- No Products State -->
|
||||
<div v-if="isMarketReady && marketStore.filteredProducts.length === 0 && !(marketStore.isLoading ?? false)" class="text-center py-12">
|
||||
<div v-if="isMarketReady && productsToDisplay.length === 0 && !(marketStore.isLoading ?? false)" class="text-center py-12">
|
||||
<h3 class="text-xl font-semibold text-gray-600 mb-2">No products found</h3>
|
||||
<p class="text-gray-500">Try adjusting your search or filters</p>
|
||||
</div>
|
||||
|
||||
<!-- Product Grid -->
|
||||
<div v-if="isMarketReady && marketStore.filteredProducts.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<div v-if="isMarketReady && productsToDisplay.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<ProductCard
|
||||
v-for="product in marketStore.filteredProducts"
|
||||
v-for="product in productsToDisplay"
|
||||
:key="product.id"
|
||||
:product="product"
|
||||
@add-to-cart="addToCart"
|
||||
|
|
@ -97,18 +98,20 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, computed } from 'vue'
|
||||
import { onMounted, onUnmounted, computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMarketStore } from '@/modules/market/stores/market'
|
||||
import { useMarket } from '../composables/useMarket'
|
||||
import { useMarketPreloader } from '../composables/useMarketPreloader'
|
||||
import { config } from '@/lib/config'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { ShoppingCart } from 'lucide-vue-next'
|
||||
import MarketFuzzySearch from '../components/MarketFuzzySearch.vue'
|
||||
import ProductCard from '../components/ProductCard.vue'
|
||||
import type { Product } from '../types/market'
|
||||
import type { FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
||||
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
|
|
@ -117,6 +120,29 @@ const marketPreloader = useMarketPreloader()
|
|||
|
||||
let unsubscribe: (() => void) | null = null
|
||||
|
||||
// Fuzzy search state
|
||||
const searchResults = ref<Product[]>([])
|
||||
|
||||
// Fuzzy search configuration for products and stalls
|
||||
const searchOptions: FuzzySearchOptions<Product> = {
|
||||
fuseOptions: {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.7 }, // Product name has highest weight
|
||||
{ name: 'stallName', weight: 0.5 }, // Stall name is important for discovery
|
||||
{ name: 'description', weight: 0.3 }, // Description provides context
|
||||
{ name: 'categories', weight: 0.4 } // Categories help with discovery
|
||||
],
|
||||
threshold: 0.6, // More tolerant of typos (0.0 = perfect match, 1.0 = match anything)
|
||||
ignoreLocation: true, // Don't care about where in the string the match is
|
||||
findAllMatches: true, // Find all matches, not just the first
|
||||
minMatchCharLength: 2, // Minimum length of a matched character sequence
|
||||
shouldSort: true // Sort results by score
|
||||
},
|
||||
resultLimit: 50, // Limit results for performance
|
||||
minSearchLength: 2, // Start searching after 2 characters
|
||||
matchAllWhenSearchEmpty: true
|
||||
}
|
||||
|
||||
// Check if we need to load market data
|
||||
const needsToLoadMarket = computed(() => {
|
||||
return !marketPreloader.isPreloaded.value &&
|
||||
|
|
@ -127,12 +153,34 @@ const needsToLoadMarket = computed(() => {
|
|||
// Check if market data is ready (either preloaded or loaded)
|
||||
const isMarketReady = computed(() => {
|
||||
const isLoading = marketStore.isLoading ?? false
|
||||
const ready = marketPreloader.isPreloaded.value ||
|
||||
const ready = marketPreloader.isPreloaded.value ||
|
||||
(marketStore.products.length > 0 && !isLoading)
|
||||
|
||||
|
||||
return ready
|
||||
})
|
||||
|
||||
// Products to display (either search results or filtered products)
|
||||
const productsToDisplay = computed(() => {
|
||||
// If we have search results (meaning user is searching), use those
|
||||
if (searchResults.value.length > 0 || searchResults.value.length === 0) {
|
||||
// Still need to apply category filters to search results
|
||||
let products = searchResults.value
|
||||
|
||||
// Apply category filters if any are selected
|
||||
const selectedCategories = marketStore.filterData.categories
|
||||
if (selectedCategories.length > 0) {
|
||||
products = products.filter(product =>
|
||||
product.categories?.some(cat => selectedCategories.includes(cat))
|
||||
)
|
||||
}
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
// Otherwise, use the store's filtered products
|
||||
return marketStore.filteredProducts
|
||||
})
|
||||
|
||||
const loadMarket = async () => {
|
||||
try {
|
||||
const naddr = config.market.defaultNaddr
|
||||
|
|
@ -174,6 +222,16 @@ const viewCart = () => {
|
|||
router.push('/cart')
|
||||
}
|
||||
|
||||
// Handle fuzzy search results
|
||||
const handleSearchResults = (results: Product[]) => {
|
||||
searchResults.value = results
|
||||
}
|
||||
|
||||
// Handle category filtering from fuzzy search
|
||||
const handleCategoryFilter = (category: string) => {
|
||||
marketStore.toggleCategoryFilter(category)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Only load market if it hasn't been preloaded
|
||||
if (needsToLoadMarket.value) {
|
||||
|
|
|
|||
|
|
@ -78,14 +78,13 @@
|
|||
<!-- Search and Filter Bar -->
|
||||
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search products in this stall..."
|
||||
class="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<FuzzySearch
|
||||
:data="stallProducts"
|
||||
:options="searchOptions"
|
||||
placeholder="Search products in this stall..."
|
||||
@results="handleSearchResults"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
|
|
@ -166,10 +165,12 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { ArrowLeft, Store, Search, Package, Loader2, X } from 'lucide-vue-next'
|
||||
import { ArrowLeft, Store, Package, Loader2, X } from 'lucide-vue-next'
|
||||
import FuzzySearch from '@/components/ui/fuzzy-search/FuzzySearch.vue'
|
||||
import ProductCard from '../components/ProductCard.vue'
|
||||
import ProductDetailDialog from '../components/ProductDetailDialog.vue'
|
||||
import type { Product, Stall } from '../types/market'
|
||||
import type { FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
|
@ -177,13 +178,32 @@ const marketStore = useMarketStore()
|
|||
|
||||
// State
|
||||
const isLoading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const searchResults = ref<Product[]>([])
|
||||
const sortBy = ref('name')
|
||||
const selectedCategories = ref<string[]>([])
|
||||
const showProductDetail = ref(false)
|
||||
const selectedProduct = ref<Product | null>(null)
|
||||
const logoError = ref(false)
|
||||
|
||||
// Fuzzy search configuration for stall products
|
||||
const searchOptions: FuzzySearchOptions<Product> = {
|
||||
fuseOptions: {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.8 }, // Product name has highest weight in stall view
|
||||
{ name: 'description', weight: 0.4 }, // Description is important for specific product search
|
||||
{ name: 'categories', weight: 0.3 } // Categories for filtering within stall
|
||||
],
|
||||
threshold: 0.2, // More strict matching since we're within a single stall
|
||||
ignoreLocation: true,
|
||||
findAllMatches: true,
|
||||
minMatchCharLength: 2,
|
||||
shouldSort: true
|
||||
},
|
||||
resultLimit: 100, // Less restrictive limit for stall view
|
||||
minSearchLength: 2,
|
||||
matchAllWhenSearchEmpty: true
|
||||
}
|
||||
|
||||
// Get stall ID from route params
|
||||
const stallId = computed(() => route.params.stallId as string)
|
||||
|
||||
|
|
@ -209,18 +229,12 @@ const stallCategories = computed(() => {
|
|||
// Product count
|
||||
const productCount = computed(() => stallProducts.value.length)
|
||||
|
||||
// Filtered and sorted products
|
||||
// Filtered and sorted products (using fuzzy search results when available)
|
||||
const filteredProducts = computed(() => {
|
||||
let products = [...stallProducts.value]
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
products = products.filter(p =>
|
||||
p.name.toLowerCase().includes(query) ||
|
||||
p.description?.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
// Use search results if available, otherwise use all stall products
|
||||
let products = searchResults.value.length > 0 || searchResults.value.length === 0
|
||||
? [...searchResults.value]
|
||||
: [...stallProducts.value]
|
||||
|
||||
// Filter by selected categories
|
||||
if (selectedCategories.value.length > 0) {
|
||||
|
|
@ -264,7 +278,12 @@ const toggleCategoryFilter = (category: string) => {
|
|||
|
||||
const clearFilters = () => {
|
||||
selectedCategories.value = []
|
||||
searchQuery.value = ''
|
||||
searchResults.value = []
|
||||
}
|
||||
|
||||
// Handle fuzzy search results
|
||||
const handleSearchResults = (results: Product[]) => {
|
||||
searchResults.value = results
|
||||
}
|
||||
|
||||
const viewProductDetails = (product: Product) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue