← back to Omega Watches 2

src/components/charts/StockChart.tsx

313 lines

'use client';

import { useState, useMemo } from 'react';
import {
  ResponsiveContainer, ComposedChart, Line, Area, XAxis, YAxis,
  CartesianGrid, Tooltip, ReferenceLine, Bar
} from 'recharts';
import { formatCurrency, formatDate } from '@/lib/utils';

interface DataPoint {
  date: string;
  price?: number;
  msrp?: number;
  volume?: number;
  event_type?: string;
  source?: string;
  sma20?: number;
  sma50?: number;
}

interface StockChartProps {
  data: DataPoint[];
  title?: string;
  reference?: string;
  onRangeChange?: (range: string) => void;
}

const RANGES = ['1W', '1M', '3M', '6M', '1Y', 'ALL'] as const;

function computeSMA(data: DataPoint[], period: number): (number | undefined)[] {
  return data.map((_, i) => {
    if (i < period - 1) return undefined;
    const slice = data.slice(i - period + 1, i + 1);
    const prices = slice.map(d => d.price).filter(Boolean) as number[];
    if (prices.length === 0) return undefined;
    return prices.reduce((a, b) => a + b, 0) / prices.length;
  });
}

function filterByRange(data: DataPoint[], range: string): DataPoint[] {
  if (range === 'ALL') return data;
  const now = new Date();
  const cutoff = new Date();
  switch (range) {
    case '1W': cutoff.setDate(now.getDate() - 7); break;
    case '1M': cutoff.setMonth(now.getMonth() - 1); break;
    case '3M': cutoff.setMonth(now.getMonth() - 3); break;
    case '6M': cutoff.setMonth(now.getMonth() - 6); break;
    case '1Y': cutoff.setFullYear(now.getFullYear() - 1); break;
  }
  return data.filter(d => new Date(d.date) >= cutoff);
}

const ChartTooltip = ({ active, payload, label }: any) => {
  if (!active || !payload?.length) return null;
  return (
    <div className="rounded-lg border border-navy-600 bg-navy-900 p-3 shadow-2xl">
      <p className="mb-1.5 text-xs font-medium text-navy-300">{formatDate(label)}</p>
      {payload.map((entry: any, i: number) => (
        <div key={i} className="flex items-center gap-2 text-sm">
          <span className="h-2 w-2 rounded-full" style={{ backgroundColor: entry.color }} />
          <span className="text-gray-400">{entry.name}:</span>
          <span className="font-mono font-medium" style={{ color: entry.color }}>
            {entry.name === 'Volume' ? entry.value : formatCurrency(entry.value)}
          </span>
        </div>
      ))}
    </div>
  );
};

export default function StockChart({ data, title, reference, onRangeChange }: StockChartProps) {
  const [range, setRange] = useState<string>('ALL');
  const [showMSRP, setShowMSRP] = useState(true);
  const [showSMA20, setShowSMA20] = useState(false);
  const [showSMA50, setShowSMA50] = useState(false);
  const [showVolume, setShowVolume] = useState(true);
  const [chartType, setChartType] = useState<'line' | 'area'>('area');

  const enrichedData = useMemo(() => {
    const filtered = filterByRange(data, range);
    const sma20 = computeSMA(filtered, 20);
    const sma50 = computeSMA(filtered, 50);
    return filtered.map((d, i) => ({
      ...d,
      sma20: sma20[i],
      sma50: sma50[i],
      volume: d.volume || (d.price ? 1 : 0),
    }));
  }, [data, range]);

  const priceChange = useMemo(() => {
    const prices = enrichedData.filter(d => d.price).map(d => d.price!);
    if (prices.length < 2) return null;
    const first = prices[0];
    const last = prices[prices.length - 1];
    return { value: last - first, pct: ((last - first) / first) * 100 };
  }, [enrichedData]);

  const latestMSRP = useMemo(() => {
    const msrps = enrichedData.filter(d => d.msrp).map(d => d.msrp!);
    return msrps.length > 0 ? msrps[msrps.length - 1] : null;
  }, [enrichedData]);

  const handleRange = (r: string) => {
    setRange(r);
    onRangeChange?.(r);
  };

  if (!data?.length) {
    return (
      <div className="flex h-96 items-center justify-center rounded-xl border border-navy-700 bg-navy-800/50">
        <p className="text-navy-400">Select a watch to view price history</p>
      </div>
    );
  }

  return (
    <div className="rounded-xl border border-navy-700 bg-navy-800/30">
      {/* Header */}
      <div className="flex flex-wrap items-center justify-between gap-2 border-b border-navy-700 px-4 py-3">
        <div className="flex items-center gap-3">
          {title && <h3 className="text-base font-semibold text-white">{title}</h3>}
          {reference && <span className="font-mono text-xs text-omega-400">{reference}</span>}
          {priceChange && (
            <span className={`text-sm font-medium ${priceChange.value >= 0 ? 'text-green-400' : 'text-red-400'}`}>
              {priceChange.value >= 0 ? '+' : ''}{formatCurrency(priceChange.value)}
              <span className="ml-1 text-xs">
                ({priceChange.pct >= 0 ? '+' : ''}{priceChange.pct.toFixed(1)}%)
              </span>
            </span>
          )}
        </div>
        <div className="flex items-center gap-1">
          {RANGES.map(r => (
            <button
              key={r}
              onClick={() => handleRange(r)}
              className={`rounded px-2.5 py-1 text-xs font-medium transition-colors ${
                range === r
                  ? 'bg-omega-500 text-white'
                  : 'text-gray-400 hover:bg-navy-700 hover:text-white'
              }`}
            >
              {r}
            </button>
          ))}
        </div>
      </div>

      {/* Toggles */}
      <div className="flex flex-wrap items-center gap-3 border-b border-navy-800 px-4 py-2">
        <button
          onClick={() => setChartType(chartType === 'line' ? 'area' : 'line')}
          className="text-xs text-gray-400 hover:text-white"
        >
          {chartType === 'line' ? '◇ Line' : '◆ Area'}
        </button>
        <label className="flex items-center gap-1.5 text-xs text-gray-400">
          <input type="checkbox" checked={showMSRP} onChange={() => setShowMSRP(!showMSRP)} className="accent-red-500" />
          MSRP
        </label>
        <label className="flex items-center gap-1.5 text-xs text-gray-400">
          <input type="checkbox" checked={showSMA20} onChange={() => setShowSMA20(!showSMA20)} className="accent-blue-400" />
          SMA(20)
        </label>
        <label className="flex items-center gap-1.5 text-xs text-gray-400">
          <input type="checkbox" checked={showSMA50} onChange={() => setShowSMA50(!showSMA50)} className="accent-purple-400" />
          SMA(50)
        </label>
        <label className="flex items-center gap-1.5 text-xs text-gray-400">
          <input type="checkbox" checked={showVolume} onChange={() => setShowVolume(!showVolume)} className="accent-gray-400" />
          Volume
        </label>
        {latestMSRP && (
          <span className="ml-auto text-xs text-gray-500">MSRP: {formatCurrency(latestMSRP)}</span>
        )}
      </div>

      {/* Chart */}
      <div className="p-2">
        <ResponsiveContainer width="100%" height={420}>
          <ComposedChart data={enrichedData} margin={{ top: 10, right: 15, bottom: 5, left: 15 }}>
            <defs>
              <linearGradient id="priceGrad" x1="0" y1="0" x2="0" y2="1">
                <stop offset="5%" stopColor="#c89b3c" stopOpacity={0.3} />
                <stop offset="95%" stopColor="#c89b3c" stopOpacity={0} />
              </linearGradient>
              <linearGradient id="msrpGrad" x1="0" y1="0" x2="0" y2="1">
                <stop offset="5%" stopColor="#ef4444" stopOpacity={0.15} />
                <stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
              </linearGradient>
            </defs>
            <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
            <XAxis
              dataKey="date"
              tickFormatter={v => formatDate(v)}
              stroke="#475569"
              fontSize={10}
              tickLine={false}
              axisLine={{ stroke: '#334155' }}
            />
            <YAxis
              yAxisId="price"
              tickFormatter={v => `$${(v / 1000).toFixed(0)}k`}
              stroke="#475569"
              fontSize={10}
              tickLine={false}
              axisLine={false}
              width={55}
            />
            {showVolume && (
              <YAxis
                yAxisId="volume"
                orientation="right"
                tickFormatter={v => `${v}`}
                stroke="#475569"
                fontSize={10}
                tickLine={false}
                axisLine={false}
                width={30}
              />
            )}
            <Tooltip content={<ChartTooltip />} />

            {/* Volume bars */}
            {showVolume && (
              <Bar
                yAxisId="volume"
                dataKey="volume"
                name="Volume"
                fill="#334155"
                opacity={0.4}
                barSize={4}
              />
            )}

            {/* MSRP overlay */}
            {showMSRP && (
              <Area
                yAxisId="price"
                type="stepAfter"
                dataKey="msrp"
                name="MSRP"
                fill="url(#msrpGrad)"
                stroke="#ef4444"
                strokeWidth={1.5}
                strokeDasharray="6 3"
                dot={false}
                connectNulls
              />
            )}

            {/* SMA lines */}
            {showSMA20 && (
              <Line
                yAxisId="price"
                type="monotone"
                dataKey="sma20"
                name="SMA(20)"
                stroke="#60a5fa"
                strokeWidth={1}
                dot={false}
                connectNulls
              />
            )}
            {showSMA50 && (
              <Line
                yAxisId="price"
                type="monotone"
                dataKey="sma50"
                name="SMA(50)"
                stroke="#a78bfa"
                strokeWidth={1}
                dot={false}
                connectNulls
              />
            )}

            {/* Price line/area */}
            {chartType === 'area' ? (
              <Area
                yAxisId="price"
                type="monotone"
                dataKey="price"
                name="Market Price"
                fill="url(#priceGrad)"
                stroke="#c89b3c"
                strokeWidth={2}
                dot={false}
                activeDot={{ r: 5, fill: '#c89b3c', stroke: '#fff', strokeWidth: 2 }}
                connectNulls
              />
            ) : (
              <Line
                yAxisId="price"
                type="monotone"
                dataKey="price"
                name="Market Price"
                stroke="#c89b3c"
                strokeWidth={2}
                dot={false}
                activeDot={{ r: 5, fill: '#c89b3c', stroke: '#fff', strokeWidth: 2 }}
                connectNulls
              />
            )}
          </ComposedChart>
        </ResponsiveContainer>
      </div>
    </div>
  );
}