Add useCategoryFilter composable and enhance MarketPage for category filtering

- Introduced a new `useCategoryFilter` composable to manage category filtering functionality, providing reactive state management and optimized performance for product filtering based on selected categories.
- Updated `MarketPage` to integrate the new composable, enhancing the UI with improved accessibility features, including ARIA roles and labels for category filters and active filters summary.
- Refactored category filtering logic to streamline product display based on selected categories, improving user experience in navigating and filtering products.

These changes enhance the overall functionality and usability of category filtering within the market module.
This commit is contained in:
padreug 2025-09-25 23:51:32 +02:00
parent 7334437b77
commit 03ca7525af
2 changed files with 244 additions and 44 deletions

View file

@ -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<Product[]>,
options: CategoryFilterOptions = {}
) {
// Use Set for O(1) lookups instead of array
const selectedCategories = ref<Set<string>>(new Set())
// Computed property for all available categories with counts
const allCategories = computed<CategoryItem[]>(() => {
const categoryMap = new Map<string, number>()
// 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<Product[]>(() => {
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