← back to Watches

src/components/PriceChart.jsx

486 lines

import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Chart, registerables } from 'chart.js';
import { motion, AnimatePresence } from 'framer-motion';
import { getAllMarketData, refreshMarketData } from '../services/marketDataService';
import useAutoRefresh from '../hooks/useAutoRefresh';
import MarketPrice from './MarketPrice';
import RecentSales from './RecentSales';
import TrendIndicator from './TrendIndicator';
import FreshnessIndicator from './FreshnessIndicator';
import RefreshButton from './RefreshButton';
import RetailVsMarket from './RetailVsMarket';

Chart.register(...registerables);

function PriceChart({ watch, onBack }) {
  const chartRef = useRef(null);
  const chartInstance = useRef(null);
  const [marketData, setMarketData] = useState(null);
  const [marketDataCollapsed, setMarketDataCollapsed] = useState(false);

  // Fetch market data
  const fetchMarketData = useCallback(async () => {
    if (!watch?.id) return;
    try {
      const data = getAllMarketData(watch.id);
      setMarketData(data);
    } catch (error) {
      console.error('Failed to fetch market data:', error);
    }
  }, [watch?.id]);

  // Auto-refresh hook (5 minutes)
  const { isRefreshing, lastRefresh, refresh } = useAutoRefresh(fetchMarketData, 5 * 60 * 1000);

  // Manual refresh handler
  const handleManualRefresh = useCallback(async () => {
    if (!watch?.id) return;
    const data = refreshMarketData(watch.id);
    setMarketData(data);
  }, [watch?.id]);

  // Fetch market data on mount
  useEffect(() => {
    fetchMarketData();
  }, [fetchMarketData]);

  useEffect(() => {
    if (chartRef.current) {
      const ctx = chartRef.current.getContext('2d');

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

      // Prepare data
      const years = watch.priceHistory.map(p => p.year);
      const prices = watch.priceHistory.map(p => p.price);

      // Create new chart
      chartInstance.current = new Chart(ctx, {
        type: 'line',
        data: {
          labels: years,
          datasets: [{
            label: 'Price (USD)',
            data: prices,
            borderColor: '#C8102E',
            backgroundColor: 'rgba(200, 16, 46, 0.1)',
            borderWidth: 3,
            fill: true,
            tension: 0.4,
            pointRadius: 5,
            pointHoverRadius: 8,
            pointBackgroundColor: '#C8102E',
            pointBorderColor: '#fff',
            pointBorderWidth: 2
          }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: {
            legend: {
              display: true,
              position: 'top',
              labels: {
                font: {
                  size: 14,
                  weight: 'bold'
                }
              }
            },
            tooltip: {
              backgroundColor: '#1A1A2E',
              titleFont: {
                size: 14
              },
              bodyFont: {
                size: 13
              },
              padding: 12,
              callbacks: {
                label: function(context) {
                  return `Price: $${context.parsed.y.toLocaleString()}`;
                }
              }
            }
          },
          scales: {
            y: {
              beginAtZero: false,
              ticks: {
                callback: function(value) {
                  return '$' + value.toLocaleString();
                },
                font: {
                  size: 12
                }
              },
              grid: {
                color: 'rgba(0, 0, 0, 0.05)'
              }
            },
            x: {
              ticks: {
                font: {
                  size: 12
                }
              },
              grid: {
                display: false
              }
            }
          }
        }
      });
    }

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

  const calculateStats = () => {
    const prices = watch.priceHistory.map(p => p.price);
    const originalPrice = prices[0];
    const currentPrice = prices[prices.length - 1];
    const totalAppreciation = ((currentPrice - originalPrice) / originalPrice * 100).toFixed(2);
    const yearlyAppreciation = (totalAppreciation / (watch.priceHistory[watch.priceHistory.length - 1].year - watch.priceHistory[0].year)).toFixed(2);
    const maxPrice = Math.max(...prices);
    const minPrice = Math.min(...prices);

    return {
      originalPrice,
      currentPrice,
      totalAppreciation,
      yearlyAppreciation,
      maxPrice,
      minPrice,
      totalGain: currentPrice - originalPrice
    };
  };

  const stats = calculateStats();

  return (
    <div className="space-y-6">
      <button
        onClick={onBack}
        className="flex items-center text-omega-red hover:text-red-700 font-semibold transition-colors"
      >
        ← Back to Watch List
      </button>

      {/* Watch Info */}
      <div className="bg-white rounded-lg shadow-md p-6">
        {/* Watch Image */}
        {watch.imageUrl && (
          <div className="mb-6 relative h-96 overflow-hidden rounded-lg">
            <img
              src={watch.imageUrl}
              alt={watch.model}
              className="w-full h-full object-cover"
              onError={(e) => {
                e.target.src = 'https://images.unsplash.com/photo-1523170335258-f5ed11844a49?w=800';
              }}
            />
          </div>
        )}

        <div className="gradient-omega text-white rounded-lg p-6 mb-6">
          <h2 className="text-3xl font-bold mb-2">{watch.model}</h2>
          <p className="text-lg opacity-90">{watch.series} Collection</p>
          <p className="text-sm opacity-75 mt-2">Reference: {watch.reference}</p>
          <p className="text-sm opacity-75">Year Introduced: {watch.yearIntroduced}</p>
        </div>

        <p className="text-gray-700 mb-6">{watch.description}</p>

        {/* Detailed Description */}
        {watch.detailedDescription && (
          <div className="mb-6 p-4 bg-blue-50 border-l-4 border-blue-500 rounded">
            <p className="text-gray-800 leading-relaxed">{watch.detailedDescription}</p>
          </div>
        )}

        {/* Technical Specifications */}
        {watch.specifications && (
          <div className="mt-6 border-t pt-6">
            <h3 className="text-xl font-bold text-gray-800 mb-4">Technical Specifications</h3>
            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
              {/* Case & Dimensions */}
              <div>
                <h4 className="font-semibold text-gray-700 mb-3 flex items-center">
                  <span className="mr-2">⚙️</span> Case & Dimensions
                </h4>
                <div className="space-y-2 bg-gray-50 p-4 rounded-lg">
                  <div className="flex justify-between">
                    <span className="text-gray-600">Case Size:</span>
                    <span className="font-semibold">{watch.specifications.caseSize}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Thickness:</span>
                    <span className="font-semibold">{watch.specifications.thickness}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Lug Width:</span>
                    <span className="font-semibold">{watch.specifications.lugWidth}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Weight:</span>
                    <span className="font-semibold">{watch.specifications.weight}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Material:</span>
                    <span className="font-semibold">{watch.specifications.caseMaterial}</span>
                  </div>
                </div>
              </div>

              {/* Movement & Performance */}
              <div>
                <h4 className="font-semibold text-gray-700 mb-3 flex items-center">
                  <span className="mr-2">🔧</span> Movement & Performance
                </h4>
                <div className="space-y-2 bg-gray-50 p-4 rounded-lg">
                  <div className="flex justify-between">
                    <span className="text-gray-600">Movement:</span>
                    <span className="font-semibold text-omega-red">{watch.specifications.movement}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Power Reserve:</span>
                    <span className="font-semibold">{watch.specifications.powerReserve}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Water Resistance:</span>
                    <span className="font-semibold">{watch.specifications.waterResistance}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Crystal:</span>
                    <span className="font-semibold">{watch.specifications.crystal}</span>
                  </div>
                </div>
              </div>

              {/* Design Elements */}
              <div>
                <h4 className="font-semibold text-gray-700 mb-3 flex items-center">
                  <span className="mr-2">🎨</span> Design Elements
                </h4>
                <div className="space-y-2 bg-gray-50 p-4 rounded-lg">
                  <div className="flex justify-between">
                    <span className="text-gray-600">Dial Color:</span>
                    <span className="font-semibold">{watch.specifications.dialColor}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-gray-600">Bezel:</span>
                    <span className="font-semibold">{watch.specifications.bezel}</span>
                  </div>
                </div>
              </div>

              {/* Features */}
              <div>
                <h4 className="font-semibold text-gray-700 mb-3 flex items-center">
                  <span className="mr-2">⭐</span> Key Features
                </h4>
                <div className="bg-gray-50 p-4 rounded-lg">
                  <div className="flex flex-wrap gap-2">
                    {watch.specifications.features.map((feature, idx) => (
                      <span
                        key={idx}
                        className="inline-block bg-omega-red text-white px-3 py-1 rounded-full text-xs font-semibold"
                      >
                        {feature}
                      </span>
                    ))}
                  </div>
                </div>
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Statistics Grid */}
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
        <div className="bg-white rounded-lg shadow-md p-4">
          <p className="text-sm text-gray-500 mb-1">Original Price</p>
          <p className="text-2xl font-bold text-gray-800">
            ${stats.originalPrice.toLocaleString()}
          </p>
        </div>
        <div className="bg-white rounded-lg shadow-md p-4">
          <p className="text-sm text-gray-500 mb-1">Current Price</p>
          <p className="text-2xl font-bold text-omega-red">
            ${stats.currentPrice.toLocaleString()}
          </p>
        </div>
        <div className="bg-white rounded-lg shadow-md p-4">
          <p className="text-sm text-gray-500 mb-1">Total Appreciation</p>
          <p className="text-2xl font-bold text-green-600">
            +{stats.totalAppreciation}%
          </p>
        </div>
        <div className="bg-white rounded-lg shadow-md p-4">
          <p className="text-sm text-gray-500 mb-1">Total Gain</p>
          <p className="text-2xl font-bold text-green-600">
            +${stats.totalGain.toLocaleString()}
          </p>
        </div>
      </div>

      {/* Market Data Section */}
      <motion.div
        className="bg-gradient-to-br from-gray-900 to-gray-800 rounded-lg shadow-xl overflow-hidden"
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ delay: 0.2 }}
      >
        {/* Header - always visible */}
        <div
          className="flex items-center justify-between p-4 cursor-pointer md:cursor-default border-b border-gray-700/50"
          onClick={() => setMarketDataCollapsed(!marketDataCollapsed)}
        >
          <div className="flex items-center gap-3">
            <h3 className="text-xl font-bold text-white">Live Market Data</h3>
            {marketData && (
              <FreshnessIndicator timestamp={marketData.timestamp} compact />
            )}
            {isRefreshing && (
              <span className="text-xs text-amber-400 animate-pulse">Refreshing...</span>
            )}
          </div>
          <div className="flex items-center gap-2">
            <RefreshButton onRefresh={handleManualRefresh} size="sm" />
            {/* Mobile collapse toggle */}
            <button className="md:hidden text-gray-400 hover:text-white transition-colors">
              <svg
                className={`w-5 h-5 transform transition-transform ${marketDataCollapsed ? 'rotate-180' : ''}`}
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
              </svg>
            </button>
          </div>
        </div>

        {/* Content - collapsible on mobile */}
        <AnimatePresence>
          {(!marketDataCollapsed || window.innerWidth >= 768) && marketData && (
            <motion.div
              initial={{ height: 0, opacity: 0 }}
              animate={{ height: 'auto', opacity: 1 }}
              exit={{ height: 0, opacity: 0 }}
              transition={{ duration: 0.3 }}
              className="p-4 md:p-6"
            >
              <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
                {/* Retail vs Market Comparison */}
                <RetailVsMarket
                  retailMSRP={marketData.retailMSRP}
                  marketPrice={marketData.marketPrice}
                  retailHistory={marketData.retailHistory}
                  reference={marketData.reference}
                />

                {/* Market Price & Trends */}
                <div className="space-y-4">
                  <MarketPrice priceData={marketData.marketPrice} />
                  <TrendIndicator
                    trends={marketData.trends}
                    defaultPeriod="30d"
                    showPeriodSelector={true}
                  />
                </div>
              </div>

              {/* Recent Sales with Source Links */}
              <div>
                <RecentSales sales={marketData.recentSales} limit={5} />
              </div>

              {/* Freshness indicator (full) */}
              <div className="mt-4 pt-4 border-t border-gray-700/50">
                <FreshnessIndicator timestamp={marketData.timestamp} />
              </div>
            </motion.div>
          )}
        </AnimatePresence>

        {/* Fallback when no data */}
        {!marketData && (
          <div className="p-6 text-center text-gray-400">
            <p>Loading market data...</p>
          </div>
        )}
      </motion.div>

      {/* Chart */}
      <div className="bg-white rounded-lg shadow-md p-6">
        <h3 className="text-xl font-bold text-gray-800 mb-4">Price History Chart</h3>
        <div className="h-96">
          <canvas ref={chartRef}></canvas>
        </div>
      </div>

      {/* Price History Table */}
      <div className="bg-white rounded-lg shadow-md p-6">
        <h3 className="text-xl font-bold text-gray-800 mb-4">Detailed Price History</h3>
        <div className="overflow-x-auto">
          <table className="w-full">
            <thead>
              <tr className="border-b-2 border-gray-300">
                <th className="text-left py-3 px-4">Year</th>
                <th className="text-right py-3 px-4">Price</th>
                <th className="text-right py-3 px-4">Change</th>
                <th className="text-right py-3 px-4">% Change</th>
                <th className="text-left py-3 px-4">Condition</th>
              </tr>
            </thead>
            <tbody>
              {watch.priceHistory.map((entry, index) => {
                const prevPrice = index > 0 ? watch.priceHistory[index - 1].price : entry.price;
                const change = entry.price - prevPrice;
                const percentChange = prevPrice > 0 ? ((change / prevPrice) * 100).toFixed(2) : 0;

                return (
                  <tr key={index} className="border-b hover:bg-gray-50">
                    <td className="py-3 px-4 font-semibold">{entry.year}</td>
                    <td className="py-3 px-4 text-right font-semibold">
                      ${entry.price.toLocaleString()}
                    </td>
                    <td className={`py-3 px-4 text-right font-semibold ${
                      change > 0 ? 'text-green-600' : change < 0 ? 'text-red-600' : 'text-gray-600'
                    }`}>
                      {change > 0 ? '+' : ''}{change === 0 ? '-' : `$${change.toLocaleString()}`}
                    </td>
                    <td className={`py-3 px-4 text-right font-semibold ${
                      percentChange > 0 ? 'text-green-600' : percentChange < 0 ? 'text-red-600' : 'text-gray-600'
                    }`}>
                      {percentChange > 0 ? '+' : ''}{percentChange === 0 ? '-' : `${percentChange}%`}
                    </td>
                    <td className="py-3 px-4">
                      <span className="inline-block bg-gray-200 px-2 py-1 rounded text-xs font-semibold">
                        {entry.condition}
                      </span>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

export default PriceChart;