diff --git a/src/modules/market/composables/useCategoryFilter.ts b/src/modules/market/composables/useCategoryFilter.ts new file mode 100644 index 0000000..68998e2 --- /dev/null +++ b/src/modules/market/composables/useCategoryFilter.ts @@ -0,0 +1,170 @@ +import { ref, computed, readonly, type Ref } from 'vue' +import type { Product } from '../types/market' + +export interface CategoryItem { + category: string + count: number + selected: boolean +} + +export interface CategoryFilterOptions { + caseSensitive?: boolean + includeEmpty?: boolean + minCount?: number +} + +/** + * Composable for category filtering functionality + * Provides reactive category management with optimized performance + */ +export function useCategoryFilter( + products: Ref, + options: CategoryFilterOptions = {} +) { + // Use Set for O(1) lookups instead of array + const selectedCategories = ref>(new Set()) + + // Computed property for all available categories with counts + const allCategories = computed(() => { + const categoryMap = new Map() + + // Count categories across all products + products.value.forEach(product => { + product.categories?.forEach(cat => { + if (cat && cat.trim()) { + const category = options.caseSensitive ? cat : cat.toLowerCase() + categoryMap.set(category, (categoryMap.get(category) || 0) + 1) + } + }) + }) + + // Convert to CategoryItem array with selection state + return Array.from(categoryMap.entries()) + .filter(([_, count]) => count >= (options.minCount || 1)) + .map(([category, count]) => ({ + category, + count, + selected: selectedCategories.value.has(category) + })) + .sort((a, b) => b.count - a.count) // Sort by popularity + }) + + // Optimized product filtering with early returns and efficient lookups + const filteredProducts = computed(() => { + const selectedSet = selectedCategories.value + + // Early return if no filters + if (selectedSet.size === 0) { + return products.value + } + + // Convert selected categories to array for faster iteration in some cases + const selectedArray = Array.from(selectedSet) + + return products.value.filter(product => { + // Handle empty categories + if (!product.categories?.length) { + return options.includeEmpty || false + } + + // Check if product has any selected category (optimized) + for (const cat of product.categories) { + if (cat && cat.trim()) { + const normalizedCategory = options.caseSensitive ? cat : cat.toLowerCase() + if (selectedSet.has(normalizedCategory)) { + return true // Early return on first match + } + } + } + + return false + }) + }) + + // Computed properties for UI state + const selectedCount = computed(() => selectedCategories.value.size) + + const selectedCategoryNames = computed(() => + Array.from(selectedCategories.value) + ) + + const hasActiveFilters = computed(() => selectedCategories.value.size > 0) + + // Actions with optimized reactivity + const toggleCategory = (category: string) => { + const normalizedCategory = options.caseSensitive ? category : category.toLowerCase() + const currentSet = selectedCategories.value + + // Create new Set to maintain reactivity (more efficient than copying) + if (currentSet.has(normalizedCategory)) { + const newSet = new Set(currentSet) + newSet.delete(normalizedCategory) + selectedCategories.value = newSet + } else { + const newSet = new Set(currentSet) + newSet.add(normalizedCategory) + selectedCategories.value = newSet + } + } + + const addCategory = (category: string) => { + const normalizedCategory = options.caseSensitive ? category : category.toLowerCase() + const newSet = new Set(selectedCategories.value) + newSet.add(normalizedCategory) + selectedCategories.value = newSet + } + + const removeCategory = (category: string) => { + const normalizedCategory = options.caseSensitive ? category : category.toLowerCase() + const newSet = new Set(selectedCategories.value) + newSet.delete(normalizedCategory) + selectedCategories.value = newSet + } + + const clearAllCategories = () => { + selectedCategories.value = new Set() // Create new empty Set + } + + const selectMultipleCategories = (categories: string[]) => { + const newSet = new Set(selectedCategories.value) + categories.forEach(cat => { + const normalizedCategory = options.caseSensitive ? cat : cat.toLowerCase() + newSet.add(normalizedCategory) + }) + selectedCategories.value = newSet + } + + const isSelected = (category: string): boolean => { + const normalizedCategory = options.caseSensitive ? category : category.toLowerCase() + return selectedCategories.value.has(normalizedCategory) + } + + // Category statistics + const categoryStats = computed(() => ({ + totalCategories: allCategories.value.length, + selectedCategories: selectedCategories.value.size, + filteredProductCount: filteredProducts.value.length, + totalProductCount: products.value.length + })) + + return { + // State (readonly to prevent external mutation) + selectedCategories: readonly(selectedCategories), + allCategories, + filteredProducts, + selectedCount, + selectedCategoryNames, + hasActiveFilters, + categoryStats, + + // Actions + toggleCategory, + addCategory, + removeCategory, + clearAllCategories, + selectMultipleCategories, + isSelected + } +} + +export default useCategoryFilter \ No newline at end of file diff --git a/src/modules/market/views/MarketPage.vue b/src/modules/market/views/MarketPage.vue index 85acaaf..febdf9f 100644 --- a/src/modules/market/views/MarketPage.vue +++ b/src/modules/market/views/MarketPage.vue @@ -53,28 +53,47 @@ + -
-
-

Browse by Category

+
+
+

+ Browse by Category +

-
+
{{ category.category }} @@ -98,6 +118,11 @@
+ + + {{ category.selected ? `${category.category} filter is active` : `${category.category} filter is inactive` }} + +
-
+
- +
-
+
@@ -169,6 +201,7 @@ import { useRouter } from 'vue-router' import { useMarketStore } from '@/modules/market/stores/market' import { useMarket } from '../composables/useMarket' import { useMarketPreloader } from '../composables/useMarketPreloader' +import { useCategoryFilter } from '../composables/useCategoryFilter' import { config } from '@/lib/config' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -184,6 +217,18 @@ const marketStore = useMarketStore() const market = useMarket() const marketPreloader = useMarketPreloader() +// Category filtering with optimized composable +const { + allCategories, + filteredProducts: categoryFilteredProducts, + selectedCount: selectedCategoriesCount, + selectedCategoryNames: selectedCategories, + hasActiveFilters, + toggleCategory, + clearAllCategories: clearAllCategoryFilters, + categoryStats +} = useCategoryFilter(computed(() => marketStore.products)) + let unsubscribe: (() => void) | null = null // Fuzzy search state @@ -225,26 +270,25 @@ const isMarketReady = computed(() => { return ready }) -// Products to display (either search results or filtered products) +// Products to display (combines search results with category filtering) 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 + // Start with either search results or all products + const baseProducts = searchResults.value.length > 0 + ? searchResults.value + : marketStore.products - // 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 + // Apply category filtering using our composable + if (!hasActiveFilters.value) { + return baseProducts } - // Otherwise, use the store's filtered products - return marketStore.filteredProducts + // Filter base products by selected categories + return baseProducts.filter(product => { + if (!product.categories?.length) return false + return product.categories.some(cat => + selectedCategories.value.includes(cat.toLowerCase()) + ) + }) }) const loadMarket = async () => { @@ -295,21 +339,7 @@ const handleSearchResults = (results: Product[]) => { // Handle category filtering from fuzzy search const handleCategoryFilter = (category: string) => { - marketStore.toggleCategoryFilter(category) -} - -// Computed properties for enhanced category filtering -const selectedCategoriesCount = computed(() => { - return marketStore.filterData.categories.length -}) - -const selectedCategories = computed(() => { - return marketStore.filterData.categories -}) - -// Clear all category filters -const clearAllCategoryFilters = () => { - marketStore.clearCategoryFilters() + toggleCategory(category) } onMounted(() => {