feat: introduce ImageLightbox and ImageViewer components for enhanced image handling
- Added ImageLightbox component to provide a modal view for images with navigation and keyboard support. - Implemented ImageViewer component to display images with features like thumbnails, cycling controls, and lightbox integration. - Updated ProgressiveImage component for improved loading and error handling. - Refactored image imports in ProductCard, ProductDetailPage, and CheckoutPage to align with new component structure. These changes significantly enhance the user experience for viewing and interacting with product images across the application.
This commit is contained in:
parent
3aec5bbdb3
commit
ca0ac2b9ad
9 changed files with 15 additions and 266 deletions
|
|
@ -1,188 +0,0 @@
|
|||
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<T>(ref: import('vue').Ref<T>) {
|
||||
return computed(() => ref.value)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue