web-app/src/modules/market/components/ProductGrid.vue
padreug c8860dc937 feat: extract and consolidate common UI patterns across market module
## 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
2025-09-27 09:45:33 +02:00

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>