← back to Watches

src/components/WatchComparison.jsx

250 lines

import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FiX, FiPlus, FiTrendingUp, FiDollarSign } from 'react-icons/fi';
import { Line } from 'react-chartjs-2';

/**
 * Side-by-side watch comparison component
 * Compare up to 3 watches with specs and price charts
 */
function WatchComparison({ watches, selectedWatches = [], onClose, onAddWatch, onRemoveWatch }) {
  const maxCompare = 3;
  const canAddMore = selectedWatches.length < maxCompare;

  const calculateAppreciation = (watch) => {
    if (watch.priceHistory.length < 2) return 0;
    const first = watch.priceHistory[0].price;
    const last = watch.priceHistory[watch.priceHistory.length - 1].price;
    return (((last - first) / first) * 100).toFixed(2);
  };

  const getChartData = () => {
    const datasets = selectedWatches.map((watch, index) => {
      const colors = ['#C8102E', '#FFD700', '#1A1A2E'];
      return {
        label: watch.model,
        data: watch.priceHistory.map(p => ({ x: p.date, y: p.price })),
        borderColor: colors[index],
        backgroundColor: `${colors[index]}20`,
        borderWidth: 3,
        fill: false,
        tension: 0.4
      };
    });

    return { datasets };
  };

  const chartOptions = {
    responsive: true,
    maintainAspectRatio: false,
    interaction: {
      mode: 'index',
      intersect: false
    },
    plugins: {
      legend: {
        position: 'top',
        labels: {
          usePointStyle: true,
          padding: 15,
          font: { size: 12, weight: 'bold' }
        }
      },
      tooltip: {
        callbacks: {
          label: (context) => {
            return `${context.dataset.label}: $${context.parsed.y.toLocaleString()}`;
          }
        }
      }
    },
    scales: {
      x: {
        type: 'time',
        time: { unit: 'year' },
        grid: { display: false }
      },
      y: {
        ticks: {
          callback: (value) => `$${(value / 1000).toFixed(0)}K`
        }
      }
    }
  };

  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
      onClick={onClose}
    >
      <motion.div
        initial={{ scale: 0.9, opacity: 0 }}
        animate={{ scale: 1, opacity: 1 }}
        exit={{ scale: 0.9, opacity: 0 }}
        onClick={(e) => e.stopPropagation()}
        className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-7xl w-full max-h-[90vh] overflow-hidden"
      >
        {/* Header */}
        <div className="gradient-omega text-white p-6 flex items-center justify-between">
          <div>
            <h2 className="text-2xl font-bold">Watch Comparison</h2>
            <p className="text-sm opacity-90">
              Compare up to {maxCompare} watches side-by-side
            </p>
          </div>
          <button
            onClick={onClose}
            className="p-2 hover:bg-white/20 rounded-lg transition-colors"
          >
            <FiX className="text-2xl" />
          </button>
        </div>

        <div className="p-6 overflow-y-auto max-h-[calc(90vh-100px)]">
          {/* Price Comparison Chart */}
          {selectedWatches.length > 0 && (
            <div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-6 mb-6">
              <h3 className="text-lg font-bold text-gray-800 dark:text-white mb-4">
                Price History Comparison
              </h3>
              <div className="h-64">
                <Line data={getChartData()} options={chartOptions} />
              </div>
            </div>
          )}

          {/* Watch Cards Grid */}
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {selectedWatches.map((watch, index) => (
              <ComparisonCard
                key={watch.id}
                watch={watch}
                index={index}
                appreciation={calculateAppreciation(watch)}
                onRemove={() => onRemoveWatch(watch.id)}
              />
            ))}

            {/* Add Watch Card */}
            {canAddMore && (
              <div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 flex flex-col items-center justify-center min-h-[400px] hover:border-omega-red transition-colors cursor-pointer">
                <FiPlus className="text-4xl text-gray-400 mb-4" />
                <p className="text-gray-500 dark:text-gray-400 text-center">
                  Add another watch to compare
                </p>
                <select
                  onChange={(e) => {
                    if (e.target.value) {
                      const watch = watches.find(w => w.id === e.target.value);
                      if (watch) onAddWatch(watch);
                      e.target.value = '';
                    }
                  }}
                  className="mt-4 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-omega-red dark:bg-gray-700 dark:text-white"
                >
                  <option value="">Select a watch...</option>
                  {watches
                    .filter(w => !selectedWatches.find(sw => sw.id === w.id))
                    .map(watch => (
                      <option key={watch.id} value={watch.id}>
                        {watch.model}
                      </option>
                    ))}
                </select>
              </div>
            )}
          </div>
        </div>
      </motion.div>
    </motion.div>
  );
}

function ComparisonCard({ watch, index, appreciation, onRemove }) {
  const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
  const originalPrice = watch.priceHistory[0].price;
  const colors = ['border-red-500', 'border-yellow-500', 'border-blue-900'];

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ delay: index * 0.1 }}
      className={`bg-white dark:bg-gray-800 rounded-lg border-4 ${colors[index]} overflow-hidden relative`}
    >
      <button
        onClick={onRemove}
        className="absolute top-2 right-2 z-10 bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors"
      >
        <FiX />
      </button>

      {watch.imageUrl && (
        <div className="h-48 overflow-hidden bg-gray-100 dark:bg-gray-700">
          <img
            src={watch.imageUrl}
            alt={watch.model}
            className="w-full h-full object-cover"
          />
        </div>
      )}

      <div className="p-4 space-y-4">
        <div>
          <h3 className="font-bold text-lg text-gray-800 dark:text-white">{watch.model}</h3>
          <p className="text-sm text-gray-500 dark:text-gray-400">{watch.series}</p>
          <p className="text-xs font-mono text-gray-600 dark:text-gray-400 mt-1">{watch.reference}</p>
        </div>

        {/* Price Stats */}
        <div className="grid grid-cols-2 gap-4">
          <div className="bg-gray-50 dark:bg-gray-700 p-3 rounded-lg">
            <p className="text-xs text-gray-500 dark:text-gray-400">Original</p>
            <p className="font-bold text-gray-800 dark:text-white">${originalPrice.toLocaleString()}</p>
          </div>
          <div className="bg-gray-50 dark:bg-gray-700 p-3 rounded-lg">
            <p className="text-xs text-gray-500 dark:text-gray-400">Current</p>
            <p className="font-bold text-omega-red">${currentPrice.toLocaleString()}</p>
          </div>
        </div>

        {/* Appreciation */}
        <div className="bg-gradient-to-r from-omega-navy to-omega-red text-white p-3 rounded-lg">
          <div className="flex items-center justify-between">
            <span className="text-sm">Appreciation</span>
            <span className="text-2xl font-bold">
              {parseFloat(appreciation) > 0 ? '+' : ''}{appreciation}%
            </span>
          </div>
        </div>

        {/* Specifications */}
        {watch.specifications && (
          <div className="space-y-2 text-sm">
            <SpecRow label="Case" value={watch.specifications.caseSize} />
            <SpecRow label="Movement" value={watch.specifications.movement} />
            <SpecRow label="Material" value={watch.specifications.caseMaterial} />
            <SpecRow label="Water Resistance" value={watch.specifications.waterResistance} />
            <SpecRow label="Year" value={watch.yearIntroduced} />
          </div>
        )}
      </div>
    </motion.div>
  );
}

function SpecRow({ label, value }) {
  return (
    <div className="flex justify-between py-1 border-b border-gray-200 dark:border-gray-700">
      <span className="text-gray-500 dark:text-gray-400">{label}:</span>
      <span className="font-semibold text-gray-800 dark:text-white">{value}</span>
    </div>
  );
}

export default WatchComparison;