/** * Product Card Carousel * Lightweight swipe carousel for product cards on shop page * Touch swipe, mouse drag, dot navigation with circular wrap */ (function() { 'use strict'; // Configuration const CONFIG = { swipeThreshold: 50, // Min pixels to trigger swipe transitionDuration: 300, // ms debug: false // Set to true for console logging }; function log(...args) { if (CONFIG.debug) console.log('[Carousel]', ...args); } /** * Initialize carousel on a product card */ function initCarousel(card) { const carousel = card.querySelector('.product-carousel'); if (!carousel || carousel.dataset.initialized === 'true') return; const imagesData = carousel.dataset.images; if (!imagesData) { log('No images data on carousel'); return; } let images; try { images = JSON.parse(imagesData.replace(/"/g, '"')); } catch (e) { console.warn('Failed to parse carousel images', e); return; } if (!images || images.length <= 1) { log('Only 1 or no images, skipping:', images ? images.length : 0); return; } log('Initializing carousel with', images.length, 'images:', images); // Mark as initialized carousel.dataset.initialized = 'true'; // State let currentIndex = 0; let startX = 0; let startY = 0; let isDragging = false; let isHorizontalSwipe = null; let hasMoved = false; const track = carousel.querySelector('.carousel-track'); const dotsContainer = carousel.querySelector('.carousel-dots'); // Build additional slides buildSlides(track, images); // Build dots buildDots(dotsContainer, images.length); const dots = dotsContainer.querySelectorAll('.carousel-dot'); // Touch handlers carousel.addEventListener('touchstart', handleTouchStart, { passive: true }); carousel.addEventListener('touchmove', handleTouchMove, { passive: false }); carousel.addEventListener('touchend', handleTouchEnd); // Mouse handlers for desktop carousel.addEventListener('mousedown', handleMouseDown); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseEnd); // Dot click handlers dots.forEach((dot, index) => { dot.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); goToSlide(index); }); }); // Prevent link navigation on swipe const link = card.querySelector('.product-card-link'); if (link) { link.addEventListener('click', (e) => { if (hasMoved) { e.preventDefault(); e.stopPropagation(); } }); } function handleTouchStart(e) { startX = e.touches[0].clientX; startY = e.touches[0].clientY; isDragging = true; isHorizontalSwipe = null; hasMoved = false; } function handleTouchMove(e) { if (!isDragging) return; const currentX = e.touches[0].clientX; const currentY = e.touches[0].clientY; const diffX = currentX - startX; const diffY = currentY - startY; // Determine swipe direction on first significant move if (isHorizontalSwipe === null && (Math.abs(diffX) > 10 || Math.abs(diffY) > 10)) { isHorizontalSwipe = Math.abs(diffX) > Math.abs(diffY); } // Only handle horizontal swipes if (isHorizontalSwipe) { e.preventDefault(); // Prevent vertical scroll hasMoved = true; } } function handleTouchEnd(e) { if (!isDragging) return; const endX = e.changedTouches[0].clientX; const diff = endX - startX; if (isHorizontalSwipe && Math.abs(diff) > CONFIG.swipeThreshold) { if (diff > 0) { goToPrev(); } else { goToNext(); } } isDragging = false; isHorizontalSwipe = null; // Reset hasMoved after a short delay to allow link click to be prevented setTimeout(() => { hasMoved = false; }, 50); } function handleMouseDown(e) { // Only left click if (e.button !== 0) return; startX = e.clientX; isDragging = true; hasMoved = false; e.preventDefault(); } function handleMouseMove(e) { if (!isDragging) return; const diff = Math.abs(e.clientX - startX); if (diff > 5) { hasMoved = true; } } function handleMouseEnd(e) { if (!isDragging) return; const diff = e.clientX - startX; if (Math.abs(diff) > CONFIG.swipeThreshold) { if (diff > 0) { goToPrev(); } else { goToNext(); } } isDragging = false; // Reset hasMoved after a short delay setTimeout(() => { hasMoved = false; }, 50); } function goToSlide(index) { // Circular navigation if (index < 0) index = images.length - 1; if (index >= images.length) index = 0; log('Going to slide', index); currentIndex = index; // Update track position track.style.transform = `translateX(-${currentIndex * 100}%)`; // Update dots dots.forEach((dot, i) => { dot.classList.toggle('active', i === currentIndex); }); } function goToNext() { goToSlide(currentIndex + 1); } function goToPrev() { goToSlide(currentIndex - 1); } } /** * Build additional carousel slides */ function buildSlides(track, images) { // First slide already exists, build remaining // Load images directly (no lazy loading) for better UX for (let i = 1; i < images.length; i++) { const slide = document.createElement('div'); slide.className = 'carousel-slide'; slide.innerHTML = `