## Component Extraction
- Create MarketSearchBar component with dual-mode support (enhanced/simple)
- Enhanced mode: suggestions, keyboard shortcuts, category filters
- Simple mode: basic search functionality
- Dynamic imports for performance optimization
- Extract LoadingErrorState component for consistent loading/error handling
- Configurable compact/full modes with custom messages
- Built-in retry functionality
- Standardized spinner and error displays
- Consolidate CartButton component (already extracted in previous commit)
## UI Standardization
- Replace inline category badges in StallView with CategoryFilterBar component
- Add missing state management for category filtering (filterMode, setFilterMode)
- Ensure consistent filtering UI between MarketPage and StallView
- Standardize loading states across MarketPage, ProductGrid, and MerchantStore
## Code Organization
- MarketPage: Uses enhanced MarketSearchBar with full feature set
- StallView: Uses simple MarketSearchBar for cleaner stall-specific search
- Both views now share CategoryFilterBar, CartButton, and ProductGrid
- LoadingErrorState provides unified loading/error UX patterns
## Technical Improvements
- Eliminate code duplication following DRY principles
- Improve maintainability with single source of truth for UI patterns
- Optimize performance with conditional feature loading
- Enhance accessibility with consistent keyboard shortcuts and ARIA labels
- Ensure mobile-responsive designs with unified behavior
BREAKING CHANGE: MarketFuzzySearch component replaced by MarketSearchBar
127 lines
No EOL
3.5 KiB
Vue
127 lines
No EOL
3.5 KiB
Vue
<template>
|
|
<div class="product-grid-container">
|
|
<LoadingErrorState
|
|
:is-loading="isLoading"
|
|
:loading-message="loadingMessage"
|
|
:has-error="false"
|
|
:full-height="false"
|
|
>
|
|
<!-- Empty State -->
|
|
<div v-if="products.length === 0" class="text-center py-12">
|
|
<slot name="empty">
|
|
<EmptyIcon class="w-24 h-24 text-muted-foreground/50 mx-auto mb-4" />
|
|
<h3 class="text-xl font-semibold text-gray-600 mb-2">{{ emptyTitle }}</h3>
|
|
<p class="text-gray-500">{{ emptyMessage }}</p>
|
|
</slot>
|
|
</div>
|
|
|
|
<!-- Product Grid -->
|
|
<div v-else :class="gridClasses">
|
|
<ProductCard
|
|
v-for="product in products"
|
|
:key="product.id"
|
|
:product="product as Product"
|
|
@add-to-cart="handleAddToCart"
|
|
@view-details="handleViewDetails"
|
|
@view-stall="$emit('view-stall', $event)"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Product Detail Dialog - Now managed internally -->
|
|
<ProductDetailDialog
|
|
v-if="selectedProduct"
|
|
:product="selectedProduct"
|
|
:isOpen="showProductDetail"
|
|
@close="closeProductDetail"
|
|
@add-to-cart="handleDialogAddToCart"
|
|
/>
|
|
</LoadingErrorState>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { Package as EmptyIcon } from 'lucide-vue-next'
|
|
import ProductCard from './ProductCard.vue'
|
|
import ProductDetailDialog from './ProductDetailDialog.vue'
|
|
import LoadingErrorState from './LoadingErrorState.vue'
|
|
import type { Product } from '../types/market'
|
|
|
|
interface Props {
|
|
products: Product[]
|
|
isLoading?: boolean
|
|
loadingMessage?: string
|
|
emptyTitle?: string
|
|
emptyMessage?: string
|
|
columns?: {
|
|
mobile?: number
|
|
sm?: number
|
|
md?: number
|
|
lg?: number
|
|
xl?: number
|
|
}
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
isLoading: false,
|
|
loadingMessage: 'Loading products...',
|
|
emptyTitle: 'No products found',
|
|
emptyMessage: 'Try adjusting your filters or search terms',
|
|
columns: () => ({
|
|
mobile: 1,
|
|
sm: 2,
|
|
md: 2,
|
|
lg: 3,
|
|
xl: 4
|
|
})
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'add-to-cart': [product: Product, quantity?: number]
|
|
'view-stall': [stallId: string]
|
|
}>()
|
|
|
|
// Compute grid classes based on column configuration
|
|
const gridClasses = computed(() => {
|
|
const cols = props.columns
|
|
const classes = ['grid', 'gap-6']
|
|
|
|
// Mobile columns
|
|
if (cols.mobile === 1) classes.push('grid-cols-1')
|
|
else if (cols.mobile === 2) classes.push('grid-cols-2')
|
|
|
|
// Responsive columns
|
|
if (cols.sm) classes.push(`sm:grid-cols-${cols.sm}`)
|
|
if (cols.md) classes.push(`md:grid-cols-${cols.md}`)
|
|
if (cols.lg) classes.push(`lg:grid-cols-${cols.lg}`)
|
|
if (cols.xl) classes.push(`xl:grid-cols-${cols.xl}`)
|
|
|
|
return classes.join(' ')
|
|
})
|
|
|
|
// Internal state for product detail dialog
|
|
const showProductDetail = ref(false)
|
|
const selectedProduct = ref<Product | null>(null)
|
|
|
|
// Handle view details internally
|
|
const handleViewDetails = (product: Product) => {
|
|
selectedProduct.value = product
|
|
showProductDetail.value = true
|
|
}
|
|
|
|
const closeProductDetail = () => {
|
|
showProductDetail.value = false
|
|
selectedProduct.value = null
|
|
}
|
|
|
|
// Handle add to cart from product card (quick add, quantity 1)
|
|
const handleAddToCart = (product: Product) => {
|
|
emit('add-to-cart', product, 1)
|
|
}
|
|
|
|
// Handle add to cart from dialog (with custom quantity)
|
|
const handleDialogAddToCart = (product: Product, quantity: number) => {
|
|
emit('add-to-cart', product, quantity)
|
|
closeProductDetail()
|
|
}
|
|
</script> |