← 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;