/** * Enhanced Swipe Card Component * Provides a more modern and interactive card interface for the swipe app */ class SwipeCard { constructor(options = {}) { this.container = options.container || document.querySelector('.swipe-container'); this.onSwipe = options.onSwipe || (() => {}); this.threshold = options.threshold || 100; this.rotationFactor = options.rotationFactor || 0.05; this.scaleFactor = options.scaleFactor || 0.0005; this.transitionDuration = options.transitionDuration || 500; this.state = { isDragging: false, startX: 0, startY: 0, moveX: 0, moveY: 0, hasMoved: false, touchStartTime: 0, currentImageInfo: null }; this.card = null; this.actionHints = null; this.decisionIndicators = {}; this.init(); } init() { // Find or create the card element this.card = this.container.querySelector('.image-card') || this.createCardElement(); // Find action hints this.actionHints = { left: this.container.querySelector('.left-hint'), right: this.container.querySelector('.right-hint'), up: this.container.querySelector('.up-hint'), down: this.container.querySelector('.down-hint') }; // Create decision indicators if they don't exist this.createDecisionIndicators(); // Add event listeners this.addEventListeners(); // Add 3D tilt effect this.add3DTiltEffect(); console.log('SwipeCard initialized'); } createCardElement() { const card = document.createElement('div'); card.className = 'image-card'; card.id = 'current-card'; card.setAttribute('role', 'img'); card.setAttribute('aria-label', 'Image to be swiped'); const img = document.createElement('img'); img.src = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ELoading...%3C%2Ftext%3E%3C%2Fsvg%3E'; img.alt = 'Image'; const loadingIndicator = document.createElement('div'); loadingIndicator.className = 'loading-indicator'; loadingIndicator.innerHTML = `
Loading...
`; card.appendChild(img); card.appendChild(loadingIndicator); this.container.appendChild(card); return card; } createDecisionIndicators() { // Decision indicators removed - no longer creating visual icons this.decisionIndicators = { left: null, right: null, up: null, down: null }; } addEventListeners() { // Mouse events this.card.addEventListener('mousedown', e => this.handlePointerDown(e.clientX, e.clientY)); document.addEventListener('mousemove', e => this.handlePointerMove(e.clientX, e.clientY)); document.addEventListener('mouseup', () => this.handlePointerUp()); // Touch events this.card.addEventListener('touchstart', e => this.handlePointerDown(e.touches[0].clientX, e.touches[0].clientY), { passive: true }); this.card.addEventListener('touchmove', e => this.handlePointerMove(e.touches[0].clientX, e.touches[0].clientY), { passive: true }); this.card.addEventListener('touchend', () => this.handlePointerUp()); } add3DTiltEffect() { this.container.addEventListener('mousemove', e => { if (window.innerWidth < 992) return; // Skip on mobile // Only apply when not dragging if (this.state.isDragging) return; const rect = this.container.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Calculate rotation based on mouse position const centerX = rect.width / 2; const centerY = rect.height / 2; const rotateY = ((x - centerX) / centerX) * 5; // Max 5 degrees const rotateX = ((centerY - y) / centerY) * 5; // Max 5 degrees // Apply the transform this.card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; }); this.container.addEventListener('mouseleave', () => { // Reset transform when mouse leaves if (!this.state.isDragging) { this.card.style.transition = 'transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; this.card.style.transform = ''; setTimeout(() => { this.card.style.transition = ''; }, 500); } }); } handlePointerDown(x, y) { this.state.isDragging = true; this.state.startX = x; this.state.startY = y; this.state.hasMoved = false; this.state.touchStartTime = Date.now(); this.card.classList.add('swiping'); this.card.style.transition = ''; } handlePointerMove(x, y) { if (!this.state.isDragging) return; this.state.moveX = x - this.state.startX; this.state.moveY = y - this.state.startY; if (Math.abs(this.state.moveX) > 10 || Math.abs(this.state.moveY) > 10) { this.state.hasMoved = true; } // Apply transform with smoother rotation and scale effect this.card.style.transform = `translate(${this.state.moveX}px, ${this.state.moveY}px) rotate(${this.state.moveX * this.rotationFactor}deg) scale(${1 - Math.abs(this.state.moveX) * this.scaleFactor})`; // Show appropriate hint based on direction const absX = Math.abs(this.state.moveX); const absY = Math.abs(this.state.moveY); // Hide all hints first Object.values(this.actionHints).forEach(hint => hint && hint.classList.remove('visible')); // Show the appropriate hint based on the direction of movement if (absX > 50 || absY > 50) { if (absX > absY) { if (this.state.moveX > 0) { this.actionHints.right && this.actionHints.right.classList.add('visible'); } else { this.actionHints.left && this.actionHints.left.classList.add('visible'); } } else { if (this.state.moveY > 0) { this.actionHints.down && this.actionHints.down.classList.add('visible'); } else { this.actionHints.up && this.actionHints.up.classList.add('visible'); } } } } handlePointerUp() { if (!this.state.isDragging) return; this.state.isDragging = false; this.card.classList.remove('swiping'); // Hide all hints Object.values(this.actionHints).forEach(hint => hint && hint.classList.remove('visible')); const absX = Math.abs(this.state.moveX); const absY = Math.abs(this.state.moveY); if (this.state.hasMoved && (absX > this.threshold || absY > this.threshold)) { let direction; if (absX > absY) { direction = this.state.moveX > 0 ? 'right' : 'left'; } else { direction = this.state.moveY > 0 ? 'down' : 'up'; } this.performSwipe(direction); } else { // Animate card back to center with a spring effect this.card.style.transition = 'transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; this.card.style.transform = ''; setTimeout(() => { this.card.style.transition = ''; }, 500); } this.state.moveX = 0; this.state.moveY = 0; } performSwipe(direction) { // Show the decision indicator this.showDecisionIndicator(direction); // Add swipe animation class this.card.classList.add(`swipe-${direction}`); // Call the onSwipe callback this.onSwipe(direction); // Reset card after animation completes setTimeout(() => { this.card.classList.remove(`swipe-${direction}`); }, this.transitionDuration); } showDecisionIndicator(direction) { // Decision indicators removed - no visual feedback needed console.log(`Swipe direction: ${direction}`); } setImage(imageInfo) { this.state.currentImageInfo = imageInfo; if (!imageInfo) { this.card.innerHTML = '
No more images available.
'; return; } const cardImage = this.card.querySelector('img'); if (!cardImage) return; // Preload the image const preloadImg = new Image(); preloadImg.onload = () => { cardImage.src = imageInfo.path; // Add a subtle fade-in effect cardImage.style.opacity = 0; setTimeout(() => { cardImage.style.opacity = 1; }, 50); }; preloadImg.src = imageInfo.path; } showLoading() { this.card.classList.add('loading'); } hideLoading() { this.card.classList.remove('loading'); } } export default SwipeCard;