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:
padreug 2025-09-25 23:02:47 +02:00
parent 86d3133978
commit 8aa575ffb1
3 changed files with 470 additions and 34 deletions

View file

@ -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) {