← back to Handbag Auth Nextjs

src/components/HandbagPriceChart.tsx

225 lines

"use client";

// Historical price chart component for handbags
// Shows real price data over time from multiple merchants

import React from "react";
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
} from "recharts";
import type { MerchantPriceSeries } from "../types/priceHistory";

interface Props {
  brand: string;
  model: string;
  series: MerchantPriceSeries[];
  currency?: string;
}

// Color palette for different merchants
const MERCHANT_COLORS = [
  "#10b981", // emerald
  "#3b82f6", // blue
  "#8b5cf6", // purple
  "#f59e0b", // amber
  "#ef4444", // red
  "#06b6d4", // cyan
  "#ec4899", // pink
  "#84cc16", // lime
];

export const HandbagPriceChart: React.FC<Props> = ({
  brand,
  model,
  series,
  currency = "USD",
}) => {
  if (!series || series.length === 0) {
    return (
      <div className="rounded-xl border border-gray-200 bg-gray-50 p-6 text-center">
        <p className="text-sm text-gray-600">
          No price history data available yet.
        </p>
        <p className="mt-2 text-xs text-gray-500">
          Price snapshots are collected daily from RSS feeds.
        </p>
      </div>
    );
  }

  // Transform series data into chart format
  // Create unified date axis with all data points
  const allDates = new Set<string>();
  series.forEach((s) => {
    s.dataPoints.forEach((dp) => allDates.add(dp.date));
  });

  const sortedDates = Array.from(allDates).sort();

  const chartData = sortedDates.map((date) => {
    const point: any = { date };

    series.forEach((s) => {
      const dataPoint = s.dataPoints.find((dp) => dp.date === date);
      if (dataPoint && dataPoint.inStock) {
        point[s.merchantName] = dataPoint.price;
      }
    });

    return point;
  });

  // Calculate statistics
  const allPrices = series.flatMap((s) =>
    s.dataPoints.filter((dp) => dp.inStock).map((dp) => dp.price)
  );
  const minPrice = Math.min(...allPrices);
  const maxPrice = Math.max(...allPrices);
  const avgPrice = allPrices.reduce((a, b) => a + b, 0) / allPrices.length;

  // Calculate price change
  const latestPrices = series
    .map((s) => {
      const latest = s.dataPoints
        .filter((dp) => dp.inStock)
        .sort((a, b) => b.date.localeCompare(a.date))[0];
      return latest?.price;
    })
    .filter((p): p is number => p !== undefined);

  const currentAvgPrice =
    latestPrices.length > 0
      ? latestPrices.reduce((a, b) => a + b, 0) / latestPrices.length
      : 0;

  const earliestPrices = series
    .map((s) => {
      const earliest = s.dataPoints
        .filter((dp) => dp.inStock)
        .sort((a, b) => a.date.localeCompare(b.date))[0];
      return earliest?.price;
    })
    .filter((p): p is number => p !== undefined);

  const earliestAvgPrice =
    earliestPrices.length > 0
      ? earliestPrices.reduce((a, b) => a + b, 0) / earliestPrices.length
      : 0;

  const priceChange =
    earliestAvgPrice > 0
      ? ((currentAvgPrice - earliestAvgPrice) / earliestAvgPrice) * 100
      : 0;

  return (
    <div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
      <header className="mb-6">
        <h2 className="text-xl font-bold text-gray-900">
          Price History: {brand} {model}
        </h2>
        <div className="mt-3 flex flex-wrap gap-4 text-sm">
          <div>
            <span className="text-gray-500">Current Avg:</span>{" "}
            <span className="font-semibold text-gray-900">
              ${currentAvgPrice.toLocaleString()}
            </span>
          </div>
          <div>
            <span className="text-gray-500">Low:</span>{" "}
            <span className="font-semibold text-emerald-600">
              ${minPrice.toLocaleString()}
            </span>
          </div>
          <div>
            <span className="text-gray-500">High:</span>{" "}
            <span className="font-semibold text-red-600">
              ${maxPrice.toLocaleString()}
            </span>
          </div>
          <div>
            <span className="text-gray-500">Change:</span>{" "}
            <span
              className={`font-semibold ${
                priceChange >= 0 ? "text-red-600" : "text-emerald-600"
              }`}
            >
              {priceChange >= 0 ? "+" : ""}
              {priceChange.toFixed(1)}%
            </span>
          </div>
        </div>
      </header>

      <ResponsiveContainer width="100%" height={400}>
        <LineChart data={chartData}>
          <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
          <XAxis
            dataKey="date"
            stroke="#6b7280"
            style={{ fontSize: "12px" }}
            tickFormatter={(date) => {
              const d = new Date(date);
              return `${d.getMonth() + 1}/${d.getDate()}`;
            }}
          />
          <YAxis
            stroke="#6b7280"
            style={{ fontSize: "12px" }}
            tickFormatter={(value) => `$${value.toLocaleString()}`}
          />
          <Tooltip
            contentStyle={{
              backgroundColor: "#fff",
              border: "1px solid #e5e7eb",
              borderRadius: "8px",
              padding: "12px",
            }}
            formatter={(value: number) => [`$${value.toLocaleString()}`, ""]}
            labelFormatter={(date) => {
              const d = new Date(date);
              return d.toLocaleDateString("en-US", {
                month: "short",
                day: "numeric",
                year: "numeric",
              });
            }}
          />
          <Legend
            wrapperStyle={{ paddingTop: "20px" }}
            iconType="line"
          />
          {series.map((s, idx) => (
            <Line
              key={s.merchantId}
              type="monotone"
              dataKey={s.merchantName}
              stroke={MERCHANT_COLORS[idx % MERCHANT_COLORS.length]}
              strokeWidth={2}
              dot={{ fill: MERCHANT_COLORS[idx % MERCHANT_COLORS.length], r: 4 }}
              activeDot={{ r: 6 }}
              connectNulls={false}
            />
          ))}
        </LineChart>
      </ResponsiveContainer>

      <footer className="mt-4 border-t border-gray-200 pt-4">
        <p className="text-xs text-gray-500">
          Price data collected from {series.length} merchant{series.length !== 1 ? "s" : ""}{" "}
          over {sortedDates.length} day{sortedDates.length !== 1 ? "s" : ""}.
          Snapshots collected daily from RSS feeds.
        </p>
      </footer>
    </div>
  );
};

export default HandbagPriceChart;