feat: enhance product management with new dialog and image handling features

- Introduced ProductDetailDialog component for displaying detailed product information, including images, price, and availability.
- Implemented image cycling functionality in ProductCard for better user experience when viewing multiple product images.
- Enhanced CreateProductDialog to support image uploads with improved validation and navigation protection during form editing.
- Added logic to manage uploaded images and ensure proper handling of existing product images.
- Updated MarketPage to integrate the new ProductDetailDialog, allowing users to view product details seamlessly.

These changes significantly improve the product management experience, enhancing both the display and interaction with product images.
This commit is contained in:
padreug 2025-09-28 04:05:20 +02:00
parent f7405bc26e
commit bff158cb74
5 changed files with 561 additions and 93 deletions

View file

@ -0,0 +1,355 @@
<template>
<div class="progressive-image-gallery">
<!-- Primary image display with progressive loading -->
<div v-if="currentImage" class="primary-image relative">
<ProgressiveImage
:src="currentImage"
:alt="alt || 'Image'"
:container-class="containerClass"
:image-class="[imageClass, showLightbox ? 'cursor-pointer' : ''].join(' ')"
:blur-radius="blurRadius"
:transition-duration="transitionDuration"
:loading="loading"
:show-loading-indicator="showLoadingIndicator"
@click="showLightbox && openLightbox()"
@error="handleImageError"
/>
<Badge
v-if="showBadge && images.length > 1"
class="absolute top-2 right-2"
variant="secondary"
>
{{ currentImageIndex + 1 }} of {{ images.length }}
</Badge>
</div>
<!-- Fallback when no image -->
<div v-else :class="containerClass">
<div class="w-full h-full bg-gradient-to-br from-muted/50 to-muted flex items-center justify-center">
<div class="text-center">
<Package class="w-12 h-12 mx-auto text-muted-foreground mb-2" />
<span class="text-xs text-muted-foreground">No image available</span>
</div>
</div>
</div>
<!-- Thumbnail gallery -->
<div
v-if="showThumbnails && images.length > 1"
class="thumbnail-list flex gap-2 mt-3 overflow-x-auto"
>
<button
v-for="(image, index) in images"
:key="index"
@click="selectImage(index)"
class="thumbnail-item flex-shrink-0 rounded-md overflow-hidden border-2 transition-all"
:class="{
'border-primary': index === currentImageIndex,
'border-transparent hover:border-muted-foreground': index !== currentImageIndex
}"
>
<ProgressiveImage
:src="image"
:alt="`Thumbnail ${index + 1}`"
container-class="w-16 h-16"
image-class="w-16 h-16 object-cover"
:blur-radius="4"
:transition-duration="200"
loading="lazy"
:show-loading-indicator="false"
/>
</button>
</div>
<!-- Lightbox modal -->
<Dialog v-if="!isEmbedded" v-model:open="lightboxOpen">
<DialogContent class="max-w-4xl p-0">
<div class="relative">
<ProgressiveImage
v-if="lightboxImage"
:src="lightboxImage"
:alt="alt || 'Full size image'"
container-class="w-full"
image-class="w-full h-auto max-h-[90vh] object-contain"
:blur-radius="12"
:transition-duration="500"
:show-loading-indicator="true"
/>
<Button
@click="lightboxOpen = false"
variant="ghost"
size="icon"
class="absolute top-2 right-2 bg-background/80 backdrop-blur hover:bg-background/90"
>
<X class="h-4 w-4" />
</Button>
<!-- Navigation buttons if multiple images -->
<template v-if="images.length > 1">
<Button
@click="previousLightboxImage"
variant="ghost"
size="icon"
class="absolute left-2 top-1/2 -translate-y-1/2 bg-background/80 backdrop-blur hover:bg-background/90"
>
<ChevronLeft class="h-4 w-4" />
</Button>
<Button
@click="nextLightboxImage"
variant="ghost"
size="icon"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-background/80 backdrop-blur hover:bg-background/90"
>
<ChevronRight class="h-4 w-4" />
</Button>
</template>
<!-- Image counter -->
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 bg-background/80 backdrop-blur rounded px-2 py-1 text-sm">
{{ lightboxImageIndex + 1 }} / {{ images.length }}
</div>
</div>
</DialogContent>
</Dialog>
<!-- Embedded lightbox (when used inside another dialog) - using Teleport to escape parent containers -->
<Teleport to="body">
<div
v-if="isEmbedded && lightboxOpen"
class="fixed inset-0 z-[9999] bg-background/80 backdrop-blur-sm flex items-center justify-center p-4"
@click.self="lightboxOpen = false"
>
<div class="relative max-w-[90vw] max-h-[90vh] bg-background rounded-lg p-0 shadow-lg">
<div class="relative">
<ProgressiveImage
v-if="lightboxImage"
:src="lightboxImage"
:alt="alt || 'Full size image'"
container-class="max-w-full max-h-[90vh]"
image-class="max-w-full max-h-[90vh] object-contain rounded-lg"
:blur-radius="12"
:transition-duration="500"
:show-loading-indicator="true"
/>
<Button
@click="lightboxOpen = false"
variant="ghost"
size="icon"
class="absolute top-2 right-2 bg-background/80 backdrop-blur hover:bg-background/90"
>
<X class="h-4 w-4" />
</Button>
<!-- Navigation buttons if multiple images -->
<template v-if="images.length > 1">
<Button
@click="previousLightboxImage"
variant="ghost"
size="icon"
class="absolute left-2 top-1/2 -translate-y-1/2 bg-background/80 backdrop-blur hover:bg-background/90"
>
<ChevronLeft class="h-4 w-4" />
</Button>
<Button
@click="nextLightboxImage"
variant="ghost"
size="icon"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-background/80 backdrop-blur hover:bg-background/90"
>
<ChevronRight class="h-4 w-4" />
</Button>
</template>
<!-- Image counter -->
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 bg-background/80 backdrop-blur rounded px-2 py-1 text-sm">
{{ lightboxImageIndex + 1 }} / {{ images.length }}
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { X, ChevronLeft, ChevronRight, Package } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
} from '@/components/ui/dialog'
import ProgressiveImage from './ProgressiveImage.vue'
interface Props {
images: string[]
alt?: string
containerClass?: string
imageClass?: string
blurRadius?: number
transitionDuration?: number
loading?: 'lazy' | 'eager'
showLoadingIndicator?: boolean
showThumbnails?: boolean
showLightbox?: boolean
showBadge?: boolean
isEmbedded?: boolean
initialIndex?: number
}
const props = withDefaults(defineProps<Props>(), {
alt: '',
containerClass: 'w-full h-48 bg-muted/50',
imageClass: 'w-full h-48 object-cover',
blurRadius: 8,
transitionDuration: 400,
loading: 'lazy',
showLoadingIndicator: true,
showThumbnails: true,
showLightbox: true,
showBadge: true,
isEmbedded: false,
initialIndex: 0
})
const emit = defineEmits<{
error: [error: Event]
imageChange: [index: number, src: string]
}>()
// Component state
const currentImageIndex = ref(props.initialIndex)
const lightboxOpen = ref(false)
const lightboxImageIndex = ref(0)
// Computed properties
const filteredImages = computed(() => {
return props.images.filter(img => img && img.trim() !== '')
})
const currentImage = computed(() => {
if (filteredImages.value.length === 0) return null
const index = Math.min(currentImageIndex.value, filteredImages.value.length - 1)
return filteredImages.value[index]
})
const lightboxImage = computed(() => {
if (filteredImages.value.length === 0) return null
return filteredImages.value[lightboxImageIndex.value]
})
// Methods
const selectImage = (index: number) => {
if (index >= 0 && index < filteredImages.value.length) {
currentImageIndex.value = index
emit('imageChange', index, filteredImages.value[index])
}
}
const openLightbox = () => {
lightboxImageIndex.value = currentImageIndex.value
lightboxOpen.value = true
}
const previousLightboxImage = () => {
lightboxImageIndex.value = lightboxImageIndex.value > 0
? lightboxImageIndex.value - 1
: filteredImages.value.length - 1
}
const nextLightboxImage = () => {
lightboxImageIndex.value = lightboxImageIndex.value < filteredImages.value.length - 1
? lightboxImageIndex.value + 1
: 0
}
const handleImageError = (error: Event) => {
emit('error', error)
}
// Watch for changes in images array
watch(() => props.images, () => {
// Reset to first image if current index is out of bounds
if (currentImageIndex.value >= filteredImages.value.length) {
currentImageIndex.value = 0
}
}, { immediate: true })
// Watch for initialIndex changes
watch(() => props.initialIndex, (newIndex) => {
if (newIndex !== currentImageIndex.value) {
selectImage(newIndex)
}
}, { immediate: true })
// Keyboard navigation for lightbox
const handleKeydown = (event: KeyboardEvent) => {
if (!lightboxOpen.value) return
switch (event.key) {
case 'Escape':
lightboxOpen.value = false
break
case 'ArrowLeft':
event.preventDefault()
previousLightboxImage()
break
case 'ArrowRight':
event.preventDefault()
nextLightboxImage()
break
}
}
// Add keyboard listeners when lightbox opens
watch(lightboxOpen, (isOpen) => {
if (isOpen) {
document.addEventListener('keydown', handleKeydown)
} else {
document.removeEventListener('keydown', handleKeydown)
}
})
// Cleanup keyboard listeners on unmount
import { onBeforeUnmount } from 'vue'
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeydown)
})
// Expose methods for parent components
defineExpose({
selectImage,
openLightbox,
getCurrentIndex: () => currentImageIndex.value,
getCurrentImage: () => currentImage.value
})
</script>
<style scoped>
.progressive-image-gallery {
@apply w-full;
}
.thumbnail-list {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground)) transparent;
}
.thumbnail-list::-webkit-scrollbar {
height: 6px;
}
.thumbnail-list::-webkit-scrollbar-track {
background: transparent;
}
.thumbnail-list::-webkit-scrollbar-thumb {
background-color: hsl(var(--muted-foreground));
border-radius: 3px;
}
.thumbnail-list::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--foreground));
}
</style>

View file

@ -127,11 +127,17 @@
<FormField name="images"> <FormField name="images">
<FormItem> <FormItem>
<FormLabel>Product Images</FormLabel> <FormLabel>Product Images</FormLabel>
<FormDescription>Add images to showcase your product</FormDescription> <FormDescription>Add up to 5 images to showcase your product. The first image will be the primary display image.</FormDescription>
<div class="text-center py-8 border-2 border-dashed rounded-lg"> <ImageUpload
<Package class="w-8 h-8 mx-auto mb-2 text-muted-foreground" /> v-model="uploadedImages"
<p class="text-sm text-muted-foreground">Image upload coming soon</p> :multiple="true"
</div> :max-files="5"
:max-size-mb="10"
:show-primary-button="true"
:disabled="isCreating"
:allow-camera="true"
placeholder="Add product photos"
/>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
@ -218,12 +224,13 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Package } from 'lucide-vue-next'
import type { NostrmarketAPI, Stall, CreateProductRequest } from '../services/nostrmarketAPI' import type { NostrmarketAPI, Stall, CreateProductRequest } from '../services/nostrmarketAPI'
import type { Product } from '../types/market' import type { Product } from '../types/market'
import { auth } from '@/composables/useAuthService' import { auth } from '@/composables/useAuthService'
import { useToast } from '@/core/composables/useToast' import { useToast } from '@/core/composables/useToast'
import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import ImageUpload from '@/modules/base/components/ImageUpload.vue'
import type { ImageUploadService } from '@/modules/base/services/ImageUploadService'
// Props and emits // Props and emits
interface Props { interface Props {
@ -242,11 +249,13 @@ const emit = defineEmits<{
// Services // Services
const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI
const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) as any const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) as any
const imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
const toast = useToast() const toast = useToast()
// Local state // Local state
const isCreating = ref(false) const isCreating = ref(false)
const createError = ref<string | null>(null) const createError = ref<string | null>(null)
const uploadedImages = ref<any[]>([]) // Track uploaded images with their metadata
// Computed properties // Computed properties
const isEditMode = computed(() => !!props.product?.id) const isEditMode = computed(() => !!props.product?.id)
@ -317,12 +326,25 @@ const updateProduct = async (formData: any) => {
price, price,
quantity, quantity,
categories, categories,
images,
active, active,
use_autoreply, use_autoreply,
autoreply_message autoreply_message
} = formData } = formData
// Get uploaded image URLs from the image service
const images: string[] = []
if (uploadedImages.value && uploadedImages.value.length > 0) {
for (const img of uploadedImages.value) {
if (img.alias) {
// Get the full URL for the image
const imageUrl = imageService.getImageUrl(img.alias)
if (imageUrl) {
images.push(imageUrl)
}
}
}
}
isCreating.value = true isCreating.value = true
createError.value = null createError.value = null
@ -388,12 +410,25 @@ const createProduct = async (formData: any) => {
price, price,
quantity, quantity,
categories, categories,
images,
active, active,
use_autoreply, use_autoreply,
autoreply_message autoreply_message
} = formData } = formData
// Get uploaded image URLs from the image service
const images: string[] = []
if (uploadedImages.value && uploadedImages.value.length > 0) {
for (const img of uploadedImages.value) {
if (img.alias) {
// Get the full URL for the image
const imageUrl = imageService.getImageUrl(img.alias)
if (imageUrl) {
images.push(imageUrl)
}
}
}
}
isCreating.value = true isCreating.value = true
createError.value = null createError.value = null
@ -470,6 +505,34 @@ watch(() => props.isOpen, async (isOpen) => {
// Reset form with appropriate initial values // Reset form with appropriate initial values
resetForm({ values: initialValues }) resetForm({ values: initialValues })
// Convert existing image URLs to the format expected by ImageUpload component
if (props.product?.images && props.product.images.length > 0) {
// For existing products, we need to convert URLs back to a format ImageUpload can display
uploadedImages.value = props.product.images.map((url, index) => {
let alias = url
// If it's a full pict-rs URL, extract just the file ID
if (url.includes('/image/original/')) {
const parts = url.split('/image/original/')
if (parts.length > 1 && parts[1]) {
alias = parts[1]
}
} else if (url.startsWith('http://') || url.startsWith('https://')) {
// Keep full URLs as-is
alias = url
}
return {
alias: alias,
delete_token: '',
isPrimary: index === 0,
details: {}
}
})
} else {
uploadedImages.value = []
}
// Wait for reactivity // Wait for reactivity
await nextTick() await nextTick()

View file

@ -1,11 +1,11 @@
<template> <template>
<Card class="overflow-hidden hover:shadow-lg transition-shadow duration-200"> <Card class="overflow-hidden hover:shadow-lg transition-shadow duration-200">
<!-- Product Image --> <!-- Product Image with Cycling and Progressive Loading -->
<div class="relative"> <div class="relative group">
<!-- Show actual image if available --> <!-- Show actual image with progressive loading if available -->
<ProgressiveImage <ProgressiveImage
v-if="product.images?.[0]" v-if="currentImage"
:src="product.images[0]" :src="currentImage"
:alt="product.name" :alt="product.name"
container-class="w-full h-48 bg-muted/50" container-class="w-full h-48 bg-muted/50"
image-class="w-full h-48 object-cover" image-class="w-full h-48 object-cover"
@ -24,6 +24,42 @@
</div> </div>
</div> </div>
<!-- Image Navigation Arrows (show on hover if multiple images) -->
<div v-if="hasMultipleImages" class="opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<!-- Previous Image Button -->
<Button
@click="previousImage"
type="button"
size="sm"
variant="secondary"
class="absolute left-2 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white border-0 h-8 w-8 p-0"
>
<ChevronLeft class="w-4 h-4" />
</Button>
<!-- Next Image Button -->
<Button
@click="nextImage"
type="button"
size="sm"
variant="secondary"
class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white border-0 h-8 w-8 p-0"
>
<ChevronRight class="w-4 h-4" />
</Button>
</div>
<!-- Image Indicators (dots) -->
<div v-if="hasMultipleImages" class="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex gap-1">
<div
v-for="(_, index) in productImages"
:key="index"
@click="setCurrentImage(index)"
class="w-2 h-2 rounded-full cursor-pointer transition-colors duration-200"
:class="currentImageIndex === index ? 'bg-white' : 'bg-white/50 hover:bg-white/70'"
/>
</div>
<!-- Add to Cart Button --> <!-- Add to Cart Button -->
<Button <Button
@click="addToCart" @click="addToCart"
@ -115,12 +151,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed, watch } from 'vue'
import { Card, CardContent, CardFooter, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardFooter, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import ProgressiveImage from '@/components/ui/ProgressiveImage.vue' import ProgressiveImage from '@/components/ui/ProgressiveImage.vue'
import { ShoppingCart, Package } from 'lucide-vue-next' import { ShoppingCart, Package, ChevronLeft, ChevronRight } from 'lucide-vue-next'
import type { Product } from '@/modules/market/stores/market' import type { Product } from '@/modules/market/stores/market'
interface Props { interface Props {
@ -136,6 +172,53 @@ const emit = defineEmits<{
}>() }>()
const imageError = ref(false) const imageError = ref(false)
const currentImageIndex = ref(0)
// Computed properties for image cycling
const productImages = computed(() => {
if (!props.product.images || props.product.images.length === 0) {
return []
}
return props.product.images.filter(img => img && img.trim() !== '')
})
const hasMultipleImages = computed(() => productImages.value.length > 1)
const currentImage = computed(() => {
if (productImages.value.length === 0) {
return null
}
return productImages.value[currentImageIndex.value]
})
// Image cycling methods
const nextImage = (event?: Event) => {
event?.stopPropagation()
if (productImages.value.length > 0) {
currentImageIndex.value = (currentImageIndex.value + 1) % productImages.value.length
}
}
const previousImage = (event?: Event) => {
event?.stopPropagation()
if (productImages.value.length > 0) {
currentImageIndex.value = currentImageIndex.value === 0
? productImages.value.length - 1
: currentImageIndex.value - 1
}
}
const setCurrentImage = (index: number, event?: Event) => {
event?.stopPropagation()
if (index >= 0 && index < productImages.value.length) {
currentImageIndex.value = index
}
}
// Reset image index when product changes
watch(() => props.product.id, () => {
currentImageIndex.value = 0
})
const addToCart = () => { const addToCart = () => {
emit('add-to-cart', props.product) emit('add-to-cart', props.product)

View file

@ -8,50 +8,22 @@
</DialogClose> </DialogClose>
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<!-- Product Images --> <!-- Product Images with Lightbox -->
<div class="space-y-4"> <div class="space-y-4">
<!-- Main Image --> <ProgressiveImageGallery
<div class="aspect-square rounded-lg overflow-hidden bg-gray-100"> :images="productImages"
<ProgressiveImage
v-if="currentImage"
:src="currentImage"
:alt="product.name" :alt="product.name"
container-class="w-full h-full" container-class="aspect-square rounded-lg overflow-hidden bg-gray-100"
image-class="w-full h-full object-cover" image-class="w-full h-full object-cover"
:blur-radius="12" :blur-radius="12"
:transition-duration="500" :transition-duration="500"
loading="lazy" :show-thumbnails="productImages.length > 1"
:show-loading-indicator="true" :show-lightbox="true"
:show-badge="productImages.length > 1"
:is-embedded="true"
@error="handleImageError" @error="handleImageError"
@image-change="handleImageChange"
/> />
<div v-else class="w-full h-full bg-gradient-to-br from-muted/50 to-muted flex items-center justify-center">
<div class="text-center">
<Package class="w-24 h-24 mx-auto text-muted-foreground mb-4" />
<span class="text-sm text-muted-foreground">No image available</span>
</div>
</div>
</div>
<!-- Image Thumbnails -->
<div v-if="productImages.length > 1" class="flex gap-2 overflow-x-auto">
<button
v-for="(image, index) in productImages"
:key="index"
@click="currentImageIndex = index"
class="relative w-20 h-20 rounded-lg overflow-hidden border-2 transition-all"
:class="currentImageIndex === index ? 'border-primary' : 'border-gray-200 hover:border-gray-400'"
>
<ProgressiveImage
:src="image"
:alt="`${product.name} - Image ${index + 1}`"
container-class="w-full h-full"
image-class="w-full h-full object-cover"
:blur-radius="6"
:transition-duration="300"
loading="lazy"
/>
</button>
</div>
</div> </div>
<!-- Product Details --> <!-- Product Details -->
@ -173,8 +145,8 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import ProgressiveImage from '@/components/ui/ProgressiveImage.vue' import ProgressiveImageGallery from '@/components/ui/ProgressiveImageGallery.vue'
import { Package, ShoppingCart, Store, Plus, Minus, X } from 'lucide-vue-next' import { ShoppingCart, Store, Plus, Minus, X } from 'lucide-vue-next'
import { useToast } from '@/core/composables/useToast' import { useToast } from '@/core/composables/useToast'
import type { Product } from '../types/market' import type { Product } from '../types/market'
@ -194,7 +166,6 @@ const toast = useToast()
// Local state // Local state
const quantity = ref(1) const quantity = ref(1)
const currentImageIndex = ref(0)
const imageLoadError = ref(false) const imageLoadError = ref(false)
// Computed properties // Computed properties
@ -205,13 +176,6 @@ const productImages = computed(() => {
return props.product.images.filter(img => img && img.trim() !== '') return props.product.images.filter(img => img && img.trim() !== '')
}) })
const currentImage = computed(() => {
if (productImages.value.length === 0) {
return null
}
return productImages.value[currentImageIndex.value]
})
// Methods // Methods
const formatPrice = (price: number, currency: string) => { const formatPrice = (price: number, currency: string) => {
if (currency === 'sat' || currency === 'sats') { if (currency === 'sat' || currency === 'sats') {
@ -246,22 +210,26 @@ const handleClose = () => {
emit('close') emit('close')
} }
const handleImageError = () => { const handleImageError = (error: Event) => {
imageLoadError.value = true imageLoadError.value = true
console.warn('Image failed to load:', error)
}
const handleImageChange = (index: number, src: string) => {
// Optional: Handle image change events if needed
console.log('Image changed to index:', index, 'src:', src)
} }
// Reset state when dialog opens/closes // Reset state when dialog opens/closes
watch(() => props.isOpen, (newVal) => { watch(() => props.isOpen, (newVal) => {
if (newVal) { if (newVal) {
quantity.value = 1 quantity.value = 1
currentImageIndex.value = 0
imageLoadError.value = false imageLoadError.value = false
} }
}) })
// Reset image index when product changes // Reset image error when product changes
watch(() => props.product?.id, () => { watch(() => props.product?.id, () => {
currentImageIndex.value = 0
imageLoadError.value = false imageLoadError.value = false
}) })
</script> </script>

View file

@ -238,7 +238,6 @@ const addToCart = (product: Product, quantity?: number) => {
marketStore.addToStallCart(product, quantity || 1) marketStore.addToStallCart(product, quantity || 1)
} }
const viewStall = (stallId: string) => { const viewStall = (stallId: string) => {
// Navigate to the stall view page // Navigate to the stall view page
router.push(`/market/stall/${stallId}`) router.push(`/market/stall/${stallId}`)