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