import { useState, useEffect, useCallback, useRef } from "react"; import { useWatchlist } from "../context/WatchlistContext.jsx"; const STATIC_ASSETS = "https://warframe.market/static/assets"; function computeStats(orders) { const sells = orders .filter((o) => o.type === "sell" && o.user?.status !== "invisible") .sort((a, b) => a.platinum - b.platinum); const buys = orders .filter((o) => o.type === "buy" && o.user?.status !== "invisible") .sort((a, b) => b.platinum - a.platinum); const lowestSell = sells.length > 0 ? sells[0].platinum : null; const highestBuy = buys.length > 0 ? buys[0].platinum : null; const activeSells = sells.length; return { lowestSell, highestBuy, activeSells }; } export default function WatchlistTable({ priceMin, priceMax, onItemSelect }) { const { watchlist, toggleItem } = useWatchlist(); const [catalog, setCatalog] = useState({}); const [ordersCache, setOrdersCache] = useState({}); const [loading, setLoading] = useState(true); const [errors, setErrors] = useState({}); const [sortKey, setSortKey] = useState("name"); const [sortAsc, setSortAsc] = useState(true); const intervalRef = useRef(null); // Fetch catalog for watchlist items 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("Failed to load catalog:", 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 rows = watchlist .filter((slug) => catalog[slug]) .map((slug) => { const item = catalog[slug]; const orders = ordersCache[slug] || []; const stats = computeStats(orders); const name = item.i18n?.en?.name || slug; const thumb = item.i18n?.en?.thumb; const tags = item.tags || []; return { slug, name, thumb, tags, ...stats, hasError: !!errors[slug] }; }); const sorted = [...rows].sort((a, b) => { let va, vb; if (sortKey === "name") { va = a.name; vb = b.name; } else if (sortKey === "platinum") { va = a.lowestSell ?? 99999; vb = b.lowestSell ?? 99999; } else if (sortKey === "buy") { va = a.highestBuy ?? -1; vb = b.highestBuy ?? -1; } else if (sortKey === "spread") { va = (a.lowestSell != null && a.highestBuy != null) ? a.lowestSell - a.highestBuy : 99999; vb = (b.lowestSell != null && b.highestBuy != null) ? b.lowestSell - b.highestBuy : 99999; } else { va = a.activeSells; vb = b.activeSells; } if (va < vb) return sortAsc ? -1 : 1; if (va > vb) return sortAsc ? 1 : -1; return 0; }); const filtered = sorted.filter((r) => { if (r.lowestSell == null && r.highestBuy == null) return true; const effective = r.lowestSell ?? 0; return effective >= priceMin && effective <= priceMax; }); const sortArrow = (key) => { if (sortKey !== key) return ""; return {sortAsc ? "\u25B2" : "\u25BC"}; }; if (loading) { return
Loading catalog...
; } if (watchlist.length === 0) { return (
Your watchlist is empty. Go to the Catalog tab and star some items.
); } return (
{filtered.map((row) => { const spread = row.lowestSell != null && row.highestBuy != null ? row.lowestSell - row.highestBuy : null; return ( onItemSelect(row.slug)}> ); })} {filtered.length === 0 && ( )}
{ setSortKey("name"); setSortAsc(sortKey === "name" ? !sortAsc : true); }}> Name{sortArrow("name")} Tags { setSortKey("platinum"); setSortAsc(sortKey === "platinum" ? !sortAsc : true); }}> Platinum{sortArrow("platinum")} { setSortKey("buy"); setSortAsc(sortKey === "buy" ? !sortAsc : true); }}> Highest Buy{sortArrow("buy")} { setSortKey("spread"); setSortAsc(sortKey === "spread" ? !sortAsc : true); }}> Spread{sortArrow("spread")} { setSortKey("sells"); setSortAsc(sortKey === "sells" ? !sortAsc : true); }}> # Sells{sortArrow("sells")}
{row.thumb && ( )} {row.name} {row.hasError && (error)}
{row.tags.slice(0, 3).map((tag) => ( {tag} ))} {row.lowestSell != null ? `${row.lowestSell}\u25BC` : "\u2014"} {row.highestBuy != null ? `${row.highestBuy}\u25B2` : "\u2014"} {spread != null ? (spread >= 0 ? `+${spread}` : spread) : "\u2014"} {row.activeSells}
No watchlist items match the price filter.
); }