WarTracker / client /src /components /SummaryChart.jsx
skander101's picture
initial commit: vibes only, probably bloated
6512187
Raw
History Blame Contribute Delete
5.18 kB
import { useState, useEffect, useCallback, useRef } from "react";
import { useWatchlist } from "../context/WatchlistContext.jsx";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
const COLORS = [
"#f0b232", "#58a6ff", "#3fb950", "#f85149", "#bc8cff",
"#d2a8ff", "#79c0ff", "#56d364", "#ff7b72", "#ffa657",
"#a5d6ff", "#7ee787",
];
function computeLowestSell(orders) {
const sells = orders
.filter((o) => o.type === "sell" && o.user?.status !== "invisible");
if (sells.length === 0) return null;
return Math.min(...sells.map((o) => o.platinum));
}
export default function SummaryChart({ priceMin, priceMax }) {
const { watchlist } = useWatchlist();
const [catalog, setCatalog] = useState({});
const [ordersCache, setOrdersCache] = useState({});
const [loading, setLoading] = useState(true);
const [errors, setErrors] = useState({});
const intervalRef = useRef(null);
useEffect(() => {
let cancelled = false;
async function loadCatalog() {
try {
const res = await fetch("/api/items");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled) {
const map = {};
for (const item of data) map[item.slug] = item;
setCatalog(map);
}
} catch (err) {
console.error("Catalog fetch failed:", err);
} finally {
if (!cancelled) setLoading(false);
}
}
loadCatalog();
return () => { cancelled = true; };
}, []);
const fetchOrders = useCallback(async () => {
for (const slug of watchlist) {
try {
const res = await fetch(`/api/orders/${encodeURIComponent(slug)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setOrdersCache((prev) => ({ ...prev, [slug]: data }));
setErrors((prev) => {
const next = { ...prev };
delete next[slug];
return next;
});
} catch (err) {
setErrors((prev) => ({ ...prev, [slug]: err.message }));
}
}
}, [watchlist]);
useEffect(() => {
if (watchlist.length === 0) return;
fetchOrders();
intervalRef.current = setInterval(fetchOrders, 60000);
return () => clearInterval(intervalRef.current);
}, [watchlist, fetchOrders]);
const chartData = watchlist
.filter((slug) => catalog[slug])
.map((slug) => {
const item = catalog[slug];
const orders = ordersCache[slug] || [];
const lowestSell = computeLowestSell(orders);
const name = item.i18n?.en?.name || slug;
const tags = item.tags || [];
return { slug, name, lowestSell, tags };
})
.filter((d) => d.lowestSell != null)
.filter((d) => d.lowestSell >= priceMin && d.lowestSell <= priceMax)
.sort((a, b) => a.lowestSell - b.lowestSell);
// Determine unique categories for coloring
const categories = [...new Set(chartData.flatMap((d) => d.tags))].sort();
const colorMap = {};
categories.forEach((cat, i) => {
colorMap[cat] = COLORS[i % COLORS.length];
});
if (loading) {
return <div className="loading"><div className="spinner" /> Loading...</div>;
}
if (watchlist.length === 0) {
return (
<div className="empty-msg">
Add items to your watchlist to see the dashboard.
</div>
);
}
if (chartData.length === 0) {
return (
<div className="empty-msg">
No watchlist items with price data. Make sure items have active orders.
</div>
);
}
// Build stacked data: for each item, one bar segment per first tag
const stackedData = chartData.map((d) => {
const entry = { name: d.name, slug: d.slug };
// Assign to first tag category
const cat = d.tags[0] || "other";
entry[cat] = d.lowestSell;
entry._cat = cat;
return entry;
});
// Get the categories actually used in stacked data
const usedCats = [...new Set(stackedData.map((d) => d._cat))];
return (
<div className="chart-wrap">
<h3>Lowest Sell Price per Watchlisted Item</h3>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={stackedData} margin={{ top: 10, right: 30, left: 0, bottom: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#30363d" />
<XAxis
dataKey="name"
tick={{ fill: "#8b949e", fontSize: 11 }}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis tick={{ fill: "#8b949e" }} />
<Tooltip
contentStyle={{
background: "#161b22",
border: "1px solid #30363d",
borderRadius: 8,
color: "#e6edf3",
}}
/>
<Legend wrapperStyle={{ color: "#8b949e" }} />
{usedCats.map((cat) => (
<Bar
key={cat}
dataKey={cat}
fill={colorMap[cat]}
stackId="stack"
name={cat}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
);
}