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