Spaces:
Paused
Paused
| import { useState, useEffect } from "react"; | |
| const STATIC_ASSETS = "https://warframe.market/static/assets"; | |
| function median(arr) { | |
| if (arr.length === 0) return null; | |
| const sorted = [...arr].sort((a, b) => a - b); | |
| const mid = Math.floor(sorted.length / 2); | |
| return sorted.length % 2 !== 0 | |
| ? sorted[mid] | |
| : (sorted[mid - 1] + sorted[mid]) / 2; | |
| } | |
| export default function ItemDetail({ slug, onBack }) { | |
| const [orders, setOrders] = useState(null); | |
| const [item, setItem] = useState(null); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState(null); | |
| useEffect(() => { | |
| let cancelled = false; | |
| async function load() { | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| const [ordersRes, catalogRes] = await Promise.all([ | |
| fetch(`/api/orders/${encodeURIComponent(slug)}`), | |
| fetch("/api/items"), | |
| ]); | |
| if (!ordersRes.ok) throw new Error(`Orders HTTP ${ordersRes.status}`); | |
| const ordersData = await ordersRes.json(); | |
| if (!cancelled) setOrders(ordersData); | |
| if (catalogRes.ok) { | |
| const catalogData = await catalogRes.json(); | |
| const found = catalogData.find((i) => i.slug === slug); | |
| if (!cancelled) setItem(found); | |
| } | |
| } catch (err) { | |
| if (!cancelled) setError(err.message); | |
| } finally { | |
| if (!cancelled) setLoading(false); | |
| } | |
| } | |
| load(); | |
| return () => { cancelled = true; }; | |
| }, [slug]); | |
| if (loading) { | |
| return <div className="loading"><div className="spinner" /> Loading orders...</div>; | |
| } | |
| if (error) { | |
| return ( | |
| <div> | |
| <button className="back-btn" onClick={onBack}>← Back</button> | |
| <div className="error-box"> | |
| <p>Failed to load orders: {error}</p> | |
| <button onClick={() => window.location.reload()}>Retry</button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const name = item?.i18n?.en?.name || slug; | |
| const thumb = item?.i18n?.en?.thumb; | |
| const onlineFilter = (o) => | |
| o.user?.status === "ingame" || o.user?.status === "online"; | |
| const sells = orders | |
| .filter((o) => o.type === "sell" && onlineFilter(o)) | |
| .sort((a, b) => a.platinum - b.platinum) | |
| .slice(0, 5); | |
| const buys = orders | |
| .filter((o) => o.type === "buy" && onlineFilter(o)) | |
| .sort((a, b) => b.platinum - a.platinum) | |
| .slice(0, 5); | |
| const allSells = orders.filter((o) => o.type === "sell" && onlineFilter(o)); | |
| const sellPrices = allSells.map((o) => o.platinum); | |
| const stats = { | |
| median: median(sellPrices), | |
| min: sellPrices.length > 0 ? Math.min(...sellPrices) : null, | |
| max: sellPrices.length > 0 ? Math.max(...sellPrices) : null, | |
| count: allSells.length, | |
| }; | |
| return ( | |
| <div> | |
| <button className="back-btn" onClick={onBack}>← Back to watchlist</button> | |
| <div className="detail-panel"> | |
| <div className="detail-header"> | |
| {thumb && ( | |
| <img src={`${STATIC_ASSETS}/${thumb}`} alt={name} /> | |
| )} | |
| <div> | |
| <h2>{name}</h2> | |
| <span style={{ color: "var(--text-muted)", fontSize: "0.85rem" }}> | |
| /v2/orders/item/{slug} | |
| </span> | |
| </div> | |
| </div> | |
| <div className="stats-grid"> | |
| <div className="stat-card"> | |
| <div className="stat-label">Median Sell</div> | |
| <div className="stat-value price-sell"> | |
| {stats.median != null ? `${stats.median}p` : "\u2014"} | |
| </div> | |
| </div> | |
| <div className="stat-card"> | |
| <div className="stat-label">Min Sell</div> | |
| <div className="stat-value price-sell"> | |
| {stats.min != null ? `${stats.min}p` : "\u2014"} | |
| </div> | |
| </div> | |
| <div className="stat-card"> | |
| <div className="stat-label">Max Sell</div> | |
| <div className="stat-value price-sell"> | |
| {stats.max != null ? `${stats.max}p` : "\u2014"} | |
| </div> | |
| </div> | |
| <div className="stat-card"> | |
| <div className="stat-label">Active Sells</div> | |
| <div className="stat-value">{stats.count}</div> | |
| </div> | |
| </div> | |
| {sells.length === 0 && buys.length === 0 ? ( | |
| <div className="empty-msg">No active orders from online/ingame users.</div> | |
| ) : ( | |
| <> | |
| {sells.length > 0 && ( | |
| <div className="orders-section"> | |
| <h3 className="price-sell">Top Sell Orders (low to high)</h3> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Price</th> | |
| <th>Qty</th> | |
| <th>User</th> | |
| <th>Rep</th> | |
| <th>Status</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {sells.map((o) => ( | |
| <tr key={o.id}> | |
| <td className="price-sell">{o.platinum}p</td> | |
| <td>{o.quantity}</td> | |
| <td>{o.user?.ingameName || "?"}</td> | |
| <td>{o.user?.reputation ?? "?"}</td> | |
| <td> | |
| <span style={{ | |
| color: o.user?.status === "ingame" ? "var(--green)" : "var(--blue)", | |
| }}> | |
| {o.user?.status} | |
| </span> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| {buys.length > 0 && ( | |
| <div className="orders-section" style={{ marginTop: 20 }}> | |
| <h3 className="price-buy">Top Buy Orders (high to low)</h3> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Price</th> | |
| <th>Qty</th> | |
| <th>User</th> | |
| <th>Rep</th> | |
| <th>Status</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {buys.map((o) => ( | |
| <tr key={o.id}> | |
| <td className="price-buy">{o.platinum}p</td> | |
| <td>{o.quantity}</td> | |
| <td>{o.user?.ingameName || "?"}</td> | |
| <td>{o.user?.reputation ?? "?"}</td> | |
| <td> | |
| <span style={{ | |
| color: o.user?.status === "ingame" ? "var(--green)" : "var(--blue)", | |
| }}> | |
| {o.user?.status} | |
| </span> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |