← back to Watches

src/components/OmegaPriceHistoryChart.jsx

313 lines

import React, { useState, useEffect, useRef } from 'react';
import { Chart, registerables } from 'chart.js';
import omegaWatchesData from '../../data/omega-watches-expanded-v3.json';

Chart.register(...registerables);

function OmegaPriceHistoryChart() {
  const [selectedModel, setSelectedModel] = useState('');
  const [selectedYear, setSelectedYear] = useState('');
  const [watches, setWatches] = useState([]);
  const [availableYears, setAvailableYears] = useState([]);
  const chartRef = useRef(null);
  const chartInstance = useRef(null);

  // Load watches data
  useEffect(() => {
    if (omegaWatchesData && omegaWatchesData.watches) {
      const watchesList = Object.entries(omegaWatchesData.watches).map(([key, value]) => ({
        id: key,
        ...value
      }));
      setWatches(watchesList);
      if (watchesList.length > 0) {
        setSelectedModel(watchesList[0].id);
      }
    }
  }, []);

  // Update available years when model changes
  useEffect(() => {
    if (selectedModel) {
      const watch = watches.find(w => w.id === selectedModel);
      if (watch && watch.priceHistory) {
        const years = [...new Set(watch.priceHistory.map(p => p.year))].sort((a, b) => a - b);
        setAvailableYears(years);
        if (years.length > 0) {
          setSelectedYear(years[0].toString());
        }
      }
    }
  }, [selectedModel, watches]);

  // Update chart when model or year changes
  useEffect(() => {
    if (!selectedModel || !selectedYear || !chartRef.current) return;

    const watch = watches.find(w => w.id === selectedModel);
    if (!watch || !watch.priceHistory) return;

    // Filter data up to selected year
    const yearNum = parseInt(selectedYear);
    const filteredHistory = watch.priceHistory.filter(p => p.year <= yearNum);

    // Separate retail prices (new condition) and auction sales (vintage/other)
    const retailPrices = filteredHistory
      .filter(p => p.condition === 'new')
      .map(p => ({ year: p.year, price: p.price }));

    const auctionSales = filteredHistory
      .filter(p => p.condition !== 'new' && (p.condition === 'vintage' || p.condition === 'used' || p.condition === 'auction'))
      .map(p => ({ year: p.year, price: p.price }));

    // Destroy previous chart
    if (chartInstance.current) {
      chartInstance.current.destroy();
    }

    const ctx = chartRef.current.getContext('2d');

    // Prepare data for chart
    const allYears = [...new Set([...retailPrices.map(p => p.year), ...auctionSales.map(p => p.year)])].sort((a, b) => a - b);
    
    const retailData = allYears.map(year => {
      const entry = retailPrices.find(p => p.year === year);
      return entry ? entry.price : null;
    });

    const auctionData = allYears.map(year => {
      const entry = auctionSales.find(p => p.year === year);
      return entry ? entry.price : null;
    });

    // Create chart
    chartInstance.current = new Chart(ctx, {
      type: 'line',
      data: {
        labels: allYears,
        datasets: [
          {
            label: 'Retail Price (New)',
            data: retailData,
            borderColor: '#2563eb',
            backgroundColor: 'rgba(37, 99, 235, 0.1)',
            borderWidth: 3,
            fill: true,
            tension: 0.4,
            pointRadius: 6,
            pointHoverRadius: 8,
            pointBackgroundColor: '#2563eb',
            pointBorderColor: '#fff',
            pointBorderWidth: 2,
            spanGaps: false
          },
          {
            label: 'Auction Sales (Vintage/Used)',
            data: auctionData,
            borderColor: '#dc2626',
            backgroundColor: 'rgba(220, 38, 38, 0.1)',
            borderWidth: 3,
            fill: true,
            tension: 0.4,
            pointRadius: 6,
            pointHoverRadius: 8,
            pointBackgroundColor: '#dc2626',
            pointBorderColor: '#fff',
            pointBorderWidth: 2,
            spanGaps: false
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            display: true,
            position: 'top',
            labels: {
              font: {
                size: 14,
                weight: 'bold'
              },
              padding: 15,
              usePointStyle: true
            }
          },
          tooltip: {
            backgroundColor: 'rgba(0, 0, 0, 0.8)',
            titleFont: {
              size: 14,
              weight: 'bold'
            },
            bodyFont: {
              size: 13
            },
            padding: 12,
            callbacks: {
              label: function(context) {
                if (context.parsed.y === null) {
                  return context.dataset.label + ': No data';
                }
                return context.dataset.label + ': $' + context.parsed.y.toLocaleString();
              }
            }
          },
          title: {
            display: true,
            text: watch.name + ' - Price History',
            font: {
              size: 18,
              weight: 'bold'
            },
            padding: 20
          }
        },
        scales: {
          y: {
            beginAtZero: false,
            ticks: {
              callback: function(value) {
                return '$' + value.toLocaleString();
              },
              font: {
                size: 12
              }
            },
            grid: {
              color: 'rgba(0, 0, 0, 0.1)'
            },
            title: {
              display: true,
              text: 'Price (USD)',
              font: {
                size: 14,
                weight: 'bold'
              }
            }
          },
          x: {
            ticks: {
              font: {
                size: 12
              }
            },
            grid: {
              display: false
            },
            title: {
              display: true,
              text: 'Year',
              font: {
                size: 14,
                weight: 'bold'
              }
            }
          }
        }
      }
    });

    return () => {
      if (chartInstance.current) {
        chartInstance.current.destroy();
      }
    };
  }, [selectedModel, selectedYear, watches]);

  const selectedWatch = watches.find(w => w.id === selectedModel);

  return (
    <div className="max-w-7xl mx-auto p-6 space-y-6">
      <div className="bg-white rounded-lg shadow-lg p-6">
        <h1 className="text-3xl font-bold text-gray-800 mb-6">Omega Watch Price History</h1>
        
        {/* Dropdowns */}
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
          <div>
            <label htmlFor="model-select" className="block text-sm font-semibold text-gray-700 mb-2">
              Model
            </label>
            <select
              id="model-select"
              value={selectedModel}
              onChange={(e) => setSelectedModel(e.target.value)}
              className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-800 bg-white"
            >
              {watches.map(watch => (
                <option key={watch.id} value={watch.id}>
                  {watch.name} ({watch.collection})
                </option>
              ))}
            </select>
          </div>

          <div>
            <label htmlFor="year-select" className="block text-sm font-semibold text-gray-700 mb-2">
              Year (Show data up to)
            </label>
            <select
              id="year-select"
              value={selectedYear}
              onChange={(e) => setSelectedYear(e.target.value)}
              className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-800 bg-white"
              disabled={!selectedModel}
            >
              {availableYears.map(year => (
                <option key={year} value={year.toString()}>
                  {year}
                </option>
              ))}
            </select>
          </div>
        </div>

        {/* Watch Info */}
        {selectedWatch && (
          <div className="bg-gray-50 rounded-lg p-4 mb-6">
            <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
              <div>
                <span className="text-sm text-gray-600">Collection:</span>
                <p className="font-semibold text-gray-800">{selectedWatch.collection}</p>
              </div>
              <div>
                <span className="text-sm text-gray-600">Reference:</span>
                <p className="font-semibold text-gray-800">{selectedWatch.reference}</p>
              </div>
              <div>
                <span className="text-sm text-gray-600">Year Introduced:</span>
                <p className="font-semibold text-gray-800">{selectedWatch.year_introduced}</p>
              </div>
            </div>
            {selectedWatch.description && (
              <div className="mt-4">
                <span className="text-sm text-gray-600">Description:</span>
                <p className="text-gray-800 mt-1">{selectedWatch.description}</p>
              </div>
            )}
          </div>
        )}

        {/* Chart */}
        <div className="bg-white rounded-lg p-6 border border-gray-200">
          <div className="h-96">
            <canvas ref={chartRef}></canvas>
          </div>
        </div>

        {/* Legend Info */}
        <div className="bg-blue-50 rounded-lg p-4 mt-4">
          <h3 className="font-semibold text-gray-800 mb-2">Chart Information:</h3>
          <ul className="space-y-1 text-sm text-gray-700">
            <li><span className="font-semibold text-blue-600">Blue Line:</span> Retail prices for new watches</li>
            <li><span className="font-semibold text-red-600">Red Line:</span> Confirmed auction sales (vintage/used condition)</li>
            <li className="mt-2 text-gray-600">Data shows prices up to the selected year</li>
          </ul>
        </div>
      </div>
    </div>
  );
}

export default OmegaPriceHistoryChart;