275 lines
8.7 KiB
JavaScript
275 lines
8.7 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="loading-spinner"></div>
|
|
<div>Loading...</div>
|
|
`;
|
|
|
|
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 = '<div class="no-images-message">No more images available.</div>';
|
|
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;
|