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:
parent
7334437b77
commit
03ca7525af
2 changed files with 244 additions and 44 deletions
|
|
@ -53,28 +53,47 @@
|
|||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Enhanced Category Filters -->
|
||||
<div v-if="marketStore.allCategories.length > 0" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-gray-700">Browse by Category</h3>
|
||||
<section
|
||||
v-if="allCategories.length > 0"
|
||||
class="mb-6"
|
||||
aria-labelledby="category-filters-heading"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 id="category-filters-heading" class="text-lg font-semibold text-gray-700">
|
||||
Browse by Category
|
||||
</h3>
|
||||
<Button
|
||||
v-if="selectedCategoriesCount > 0"
|
||||
@click="clearAllCategoryFilters"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-sm"
|
||||
:aria-label="`Clear all ${selectedCategoriesCount} selected category filters`"
|
||||
>
|
||||
Clear All ({{ selectedCategoriesCount }})
|
||||
<X class="w-4 h-4 ml-1" />
|
||||
<X class="w-4 h-4 ml-1" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div
|
||||
class="flex flex-wrap gap-3"
|
||||
role="group"
|
||||
aria-label="Filter products by category"
|
||||
>
|
||||
<div
|
||||
v-for="category in marketStore.allCategories"
|
||||
v-for="category in allCategories"
|
||||
:key="category.category"
|
||||
@click="marketStore.toggleCategoryFilter(category.category)"
|
||||
class="group relative cursor-pointer transition-all duration-200 hover:scale-105"
|
||||
:id="`category-filter-${category.category}`"
|
||||
role="button"
|
||||
:aria-pressed="category.selected"
|
||||
:aria-label="`${category.selected ? 'Remove' : 'Add'} ${category.category} filter. ${category.count} products available.`"
|
||||
:tabindex="0"
|
||||
@click="toggleCategory(category.category)"
|
||||
@keydown.enter="toggleCategory(category.category)"
|
||||
@keydown.space.prevent="toggleCategory(category.category)"
|
||||
class="group relative cursor-pointer transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1"
|
||||
>
|
||||
<Badge
|
||||
:variant="category.selected ? 'default' : 'outline'"
|
||||
|
|
@ -84,6 +103,7 @@
|
|||
'hover:bg-primary/10 hover:border-primary': !category.selected,
|
||||
'ring-2 ring-primary ring-offset-1': category.selected
|
||||
}"
|
||||
:aria-hidden="true"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ category.category }}</span>
|
||||
|
|
@ -98,6 +118,11 @@
|
|||
</div>
|
||||
</Badge>
|
||||
|
||||
<!-- Screen reader only text for selection state -->
|
||||
<span class="sr-only">
|
||||
{{ category.selected ? `${category.category} filter is active` : `${category.category} filter is inactive` }}
|
||||
</span>
|
||||
|
||||
<!-- Selection indicator -->
|
||||
<div
|
||||
v-if="category.selected"
|
||||
|
|
@ -109,30 +134,37 @@
|
|||
</div>
|
||||
|
||||
<!-- Active Filters Summary -->
|
||||
<div v-if="selectedCategoriesCount > 0" class="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<div
|
||||
v-if="selectedCategoriesCount > 0"
|
||||
class="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200"
|
||||
role="region"
|
||||
aria-label="Active category filters"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Filter class="w-4 h-4 text-blue-600" />
|
||||
<Filter class="w-4 h-4 text-blue-600" aria-hidden="true" />
|
||||
<span class="text-sm font-medium text-blue-800">
|
||||
Active Filters:
|
||||
</span>
|
||||
<div class="flex gap-1">
|
||||
<div class="flex gap-1" role="list" aria-label="Selected category filters">
|
||||
<Badge
|
||||
v-for="category in selectedCategories"
|
||||
:key="category"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
role="listitem"
|
||||
>
|
||||
{{ category }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-blue-600">
|
||||
<div class="text-sm text-blue-600" aria-live="polite">
|
||||
{{ productsToDisplay.length }} products found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- No Products State -->
|
||||
<div v-if="isMarketReady && productsToDisplay.length === 0 && !(marketStore.isLoading ?? false)" class="text-center py-12">
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue