diff --git a/src/components/ui/ImageLightbox.vue b/src/components/ui/ImageLightbox.vue new file mode 100644 index 0000000..418abce --- /dev/null +++ b/src/components/ui/ImageLightbox.vue @@ -0,0 +1,241 @@ + + + + + \ No newline at end of file diff --git a/src/components/ui/ImageViewer.vue b/src/components/ui/ImageViewer.vue new file mode 100644 index 0000000..22d70c9 --- /dev/null +++ b/src/components/ui/ImageViewer.vue @@ -0,0 +1,333 @@ + + + + + \ No newline at end of file diff --git a/src/components/ui/ProgressiveImageGallery.vue b/src/components/ui/ProgressiveImageGallery.vue deleted file mode 100644 index 935bbdb..0000000 --- a/src/components/ui/ProgressiveImageGallery.vue +++ /dev/null @@ -1,355 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/composables/useImageLightbox.ts b/src/composables/useImageLightbox.ts new file mode 100644 index 0000000..95fc5cf --- /dev/null +++ b/src/composables/useImageLightbox.ts @@ -0,0 +1,188 @@ +import { ref, computed, watch, onBeforeUnmount } from 'vue' + +export interface LightboxImage { + src: string + alt?: string +} + +export interface UseImageLightboxOptions { + /** + * Whether to enable keyboard navigation (arrow keys, escape) + */ + enableKeyboardNavigation?: boolean + + /** + * Whether to close lightbox when clicking backdrop + */ + closeOnBackdropClick?: boolean + + /** + * Whether to enable swipe gestures on touch devices + */ + enableSwipeGestures?: boolean +} + +export function useImageLightbox( + images: LightboxImage[], + options: UseImageLightboxOptions = {} +) { + const { + enableKeyboardNavigation = true, + closeOnBackdropClick = true, + enableSwipeGestures = true + } = options + + // Core reactive state + const isOpen = ref(false) + const currentIndex = ref(0) + + // Computed properties + const currentImage = computed(() => { + if (!images.length || currentIndex.value < 0) return null + return images[Math.min(currentIndex.value, images.length - 1)] + }) + + const hasPrevious = computed(() => images.length > 1) + const hasNext = computed(() => images.length > 1) + const totalImages = computed(() => images.length) + + // Navigation methods + const open = (index: number = 0) => { + if (images.length === 0) return + + currentIndex.value = Math.max(0, Math.min(index, images.length - 1)) + isOpen.value = true + + // Prevent body scroll when lightbox is open + document.body.style.overflow = 'hidden' + } + + const close = () => { + isOpen.value = false + + // Restore body scroll + document.body.style.overflow = '' + } + + const goToPrevious = () => { + if (!hasPrevious.value) return + + currentIndex.value = currentIndex.value > 0 + ? currentIndex.value - 1 + : images.length - 1 + } + + const goToNext = () => { + if (!hasNext.value) return + + currentIndex.value = currentIndex.value < images.length - 1 + ? currentIndex.value + 1 + : 0 + } + + const goToIndex = (index: number) => { + if (index < 0 || index >= images.length) return + currentIndex.value = index + } + + // Keyboard navigation + const handleKeydown = (event: KeyboardEvent) => { + if (!isOpen.value || !enableKeyboardNavigation) return + + switch (event.key) { + case 'Escape': + event.preventDefault() + close() + break + case 'ArrowLeft': + event.preventDefault() + goToPrevious() + break + case 'ArrowRight': + event.preventDefault() + goToNext() + break + case ' ': // Spacebar + event.preventDefault() + goToNext() + break + } + } + + // Touch/swipe gesture handling + let touchStartX = 0 + let touchStartY = 0 + const swipeThreshold = 50 + + const handleTouchStart = (event: TouchEvent) => { + if (!enableSwipeGestures || !isOpen.value) return + + touchStartX = event.touches[0].clientX + touchStartY = event.touches[0].clientY + } + + const handleTouchEnd = (event: TouchEvent) => { + if (!enableSwipeGestures || !isOpen.value) return + + const touchEndX = event.changedTouches[0].clientX + const touchEndY = event.changedTouches[0].clientY + + const deltaX = touchEndX - touchStartX + const deltaY = touchEndY - touchStartY + + // Only process horizontal swipes (ignore mostly vertical swipes) + if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > swipeThreshold) { + if (deltaX > 0) { + goToPrevious() + } else { + goToNext() + } + } + } + + + // Setup event listeners + watch(isOpen, (newIsOpen) => { + if (newIsOpen) { + document.addEventListener('keydown', handleKeydown) + document.addEventListener('touchstart', handleTouchStart, { passive: true }) + document.addEventListener('touchend', handleTouchEnd, { passive: true }) + } else { + document.removeEventListener('keydown', handleKeydown) + document.removeEventListener('touchstart', handleTouchStart) + document.removeEventListener('touchend', handleTouchEnd) + } + }) + + // Cleanup on unmount + onBeforeUnmount(() => { + close() + document.removeEventListener('keydown', handleKeydown) + document.removeEventListener('touchstart', handleTouchStart) + document.removeEventListener('touchend', handleTouchEnd) + }) + + return { + // State + isOpen: readonly(isOpen), + currentIndex: readonly(currentIndex), + currentImage, + + // Computed + hasPrevious, + hasNext, + totalImages, + + // Methods + open, + close, + goToPrevious, + goToNext, + goToIndex + } +} + +// Helper to create readonly refs +function readonly(ref: import('vue').Ref) { + return computed(() => ref.value) +} \ No newline at end of file diff --git a/src/modules/market/components/ProductDetailDialog.vue b/src/modules/market/components/ProductDetailDialog.vue index 45f2673..4642a68 100644 --- a/src/modules/market/components/ProductDetailDialog.vue +++ b/src/modules/market/components/ProductDetailDialog.vue @@ -10,7 +10,7 @@
-
@@ -145,7 +148,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' -import ProgressiveImageGallery from '@/components/ui/ProgressiveImageGallery.vue' +import ImageViewer from '@/components/ui/ImageViewer.vue' import { ShoppingCart, Store, Plus, Minus, X } from 'lucide-vue-next' import { useToast } from '@/core/composables/useToast' import type { Product } from '../types/market' @@ -220,6 +223,16 @@ const handleImageChange = (index: number, src: string) => { console.log('Image changed to index:', index, 'src:', src) } +const handleLightboxOpen = (index: number) => { + // Optional: Handle lightbox open events if needed + console.log('Lightbox opened at index:', index) +} + +const handleLightboxClose = () => { + // Optional: Handle lightbox close events if needed + console.log('Lightbox closed') +} + // Reset state when dialog opens/closes watch(() => props.isOpen, (newVal) => { if (newVal) { diff --git a/src/modules/market/components/ProductGrid.vue b/src/modules/market/components/ProductGrid.vue index 9f26806..7997824 100644 --- a/src/modules/market/components/ProductGrid.vue +++ b/src/modules/market/components/ProductGrid.vue @@ -27,23 +27,15 @@ />
- - \ No newline at end of file diff --git a/src/modules/market/index.ts b/src/modules/market/index.ts index 93b6bd6..cea3db1 100644 --- a/src/modules/market/index.ts +++ b/src/modules/market/index.ts @@ -154,6 +154,15 @@ export const marketModule: ModulePlugin = { title: 'Stall', requiresAuth: false } + }, + { + path: '/market/product/:productId', + name: 'product-detail', + component: () => import('./views/ProductDetailPage.vue'), + meta: { + title: 'Product Details', + requiresAuth: false + } } ] as RouteRecordRaw[], diff --git a/src/modules/market/views/ProductDetailPage.vue b/src/modules/market/views/ProductDetailPage.vue new file mode 100644 index 0000000..8a0437d --- /dev/null +++ b/src/modules/market/views/ProductDetailPage.vue @@ -0,0 +1,309 @@ + + + + + \ No newline at end of file