← back to Watches
src/components/LuxuryWatchCard.jsx
221 lines
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { motion } from 'framer-motion';
import { getMarketPrice, getTrends } from '../services/marketDataService';
import TrendIndicator from './TrendIndicator';
import FreshnessIndicator from './FreshnessIndicator';
/**
* Luxury Watch Card Component
* Features: 3D tilt effect, parallax layers, gold shimmer, haptic feedback
*/
const LuxuryWatchCard = ({ watch, onClick, index }) => {
const [rotation, setRotation] = useState({ x: 0, y: 0 });
const [isHovered, setIsHovered] = useState(false);
const cardRef = useRef(null);
// Fetch market data for this watch
const marketData = useMemo(() => {
if (!watch?.id) return null;
try {
const price = getMarketPrice(watch.id);
const trends = getTrends(watch.id);
return {
marketPrice: price.data,
trends: trends.data,
timestamp: Math.max(price.timestamp, trends.timestamp),
isStale: price.isStale || trends.isStale
};
} catch (error) {
console.error('Failed to fetch market data for card:', error);
return null;
}
}, [watch?.id]);
const handleMouseMove = (e) => {
if (!cardRef.current) return;
const card = cardRef.current;
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = ((y - centerY) / centerY) * -10;
const rotateY = ((x - centerX) / centerX) * 10;
setRotation({ x: rotateX, y: rotateY });
};
const handleMouseLeave = () => {
setRotation({ x: 0, y: 0 });
setIsHovered(false);
};
const formatPrice = (price) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(price);
};
const currentPrice = watch.priceHistory?.[watch.priceHistory.length - 1]?.price || 0;
const originalPrice = watch.priceHistory?.[0]?.price || 0;
const appreciation = originalPrice
? (((currentPrice - originalPrice) / originalPrice) * 100).toFixed(1)
: 0;
return (
<motion.div
ref={cardRef}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.6 }}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={handleMouseLeave}
onClick={() => onClick(watch)}
className="relative cursor-pointer group"
style={{
perspective: '1000px',
}}
>
<motion.div
className="luxury-card p-6 card-3d gpu-accelerated luxury-lift haptic-click"
animate={{
rotateX: rotation.x,
rotateY: rotation.y,
}}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
{/* Premium badge */}
{appreciation > 100 && (
<div className="absolute top-4 right-4 z-10">
<div className="bg-gradient-gold text-gray-900 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider shadow-gold">
Premium
</div>
</div>
)}
{/* Watch icon with parallax */}
<motion.div
className="text-6xl mb-4 text-center"
animate={{
x: isHovered ? rotation.y * 2 : 0,
y: isHovered ? rotation.x * 2 : 0,
}}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
>
<span className="animate-tick-tock inline-block">⌚</span>
</motion.div>
{/* Watch details */}
<div className="space-y-3">
<h3 className="font-display text-xl font-bold text-white group-hover:text-gold transition-colors duration-300">
{watch.model}
</h3>
<p className="text-sm text-gray-400 font-medium">{watch.series}</p>
{/* Reference number with gold accent */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className="text-gold">REF.</span>
<span>{watch.referenceNumber}</span>
</div>
{/* Price display with animation */}
<div className="pt-4 border-t border-gray-700/50">
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-gray-400">Current Value</span>
<motion.span
className="text-2xl font-bold text-gold"
animate={{ scale: isHovered ? 1.1 : 1 }}
transition={{ type: 'spring', stiffness: 300 }}
>
{formatPrice(currentPrice)}
</motion.span>
</div>
{/* Market Data Row */}
{marketData && (
<div className="flex items-center justify-between mb-2 py-2 border-y border-gray-700/30">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Market:</span>
<span className="text-sm font-semibold text-amber-400">
{formatPrice(marketData.marketPrice.low)} - {formatPrice(marketData.marketPrice.high)}
</span>
</div>
<div className="flex items-center gap-2">
<TrendIndicator trends={marketData.trends} compact />
<FreshnessIndicator timestamp={marketData.timestamp} compact />
</div>
</div>
)}
{/* Appreciation indicator */}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Since {watch.priceHistory?.[0]?.year}</span>
<motion.div
className={`flex items-center gap-1 font-bold ${
appreciation > 0 ? 'text-green-400' : 'text-red-400'
}`}
animate={{ scale: isHovered ? 1.05 : 1 }}
>
{appreciation > 0 ? (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
) : (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
<span className="text-sm">{Math.abs(appreciation)}%</span>
</motion.div>
</div>
</div>
{/* Hover overlay with gold shimmer */}
<motion.div
className="absolute inset-0 rounded-[20px] pointer-events-none"
initial={{ opacity: 0 }}
animate={{ opacity: isHovered ? 1 : 0 }}
transition={{ duration: 0.3 }}
>
<div className="absolute inset-0 bg-gradient-to-br from-yellow-500/10 via-transparent to-orange-500/10 rounded-[20px]" />
</motion.div>
</div>
{/* Bottom accent line */}
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-gold rounded-b-[20px]"
initial={{ scaleX: 0 }}
animate={{ scaleX: isHovered ? 1 : 0 }}
transition={{ duration: 0.4 }}
/>
</motion.div>
{/* Floating shadow effect */}
<motion.div
className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 w-3/4 h-6 bg-black/30 blur-xl rounded-full"
animate={{
opacity: isHovered ? 0.6 : 0.3,
scale: isHovered ? 1.1 : 1,
}}
transition={{ duration: 0.3 }}
/>
</motion.div>
);
};
export default LuxuryWatchCard;