Spaces:
Paused
Paused
| 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> | |
| ); | |
| } | |