/* P.E.S.T Web App — token prices: real prices via nc-proxy.php?action=getPrices (server-cached 5min from CoinGecko). Watchlist, picker, guest strip. */ /* ---------- live price engine: pulls from server every 5 min ---------- */ const PriceEngine = (function () { // Map: token.id → { price, change } let state = {}; let tokensRef = []; const subs = new Set(); function publish() { subs.forEach((fn) => fn()); } function applyResponse(resp) { // resp.prices is keyed by CoinGecko id ("solana", "bitcoin", …) const cg = (resp && resp.prices) || {}; const ns = {}; tokensRef.forEach((t) => { const live = t.gecko && cg[t.gecko]; if (live) { ns[t.id] = { price: typeof live.usd === "number" ? live.usd : t.price, change: typeof live.usd_24h_change === "number" ? live.usd_24h_change : t.change24h, }; } else { // Fallback: use seed values until next pull (or token has no gecko id) const prev = state[t.id]; ns[t.id] = prev || { price: t.price, change: t.change24h }; } }); state = ns; publish(); } async function pull() { try { const resp = await window.PEST_APP.api.getPrices(); applyResponse(resp); } catch (e) { /* keep current state on failure */ } } function ensure(tokens) { tokensRef = tokens || []; // Seed any missing entries so the UI has something to render before the first pull lands tokensRef.forEach((t) => { if (!state[t.id]) state[t.id] = { price: t.price, change: t.change24h }; }); } // Refresh every 5 min; first call happens on initial ensure() via useLivePrices. let timer = null, started = false; function start() { if (started) return; started = true; pull(); timer = setInterval(pull, 5 * 60 * 1000); } return { get: () => state, ensure: (tokens) => { ensure(tokens); start(); }, subscribe: (fn) => { subs.add(fn); return () => subs.delete(fn); }, refresh: pull, }; })(); function useLivePrices(tokens) { const [, force] = useState(0); if (tokens) PriceEngine.ensure(tokens); useEffect(() => PriceEngine.subscribe(() => force((x) => x + 1)), []); return PriceEngine.get(); } /* ---------- formatting ---------- */ function fmtPrice(p) { if (p >= 1000) return "$" + p.toLocaleString("en-US", { maximumFractionDigits: 0 }); if (p >= 1) return "$" + p.toFixed(2); if (p >= 0.01) return "$" + p.toFixed(4); if (p >= 0.0001) return "$" + p.toFixed(6); return "$" + p.toFixed(8); } function fmtChange(c) { return (c >= 0 ? "+" : "") + c.toFixed(2) + "%"; } /* ---------- token tile: image if logo present, else deterministic-hue symbol tile ---------- */ function TokenTile({ token, className }) { const src = window.resolveLogo(token.logo); if (src) { return (
{token.symbol}
); } const h = hueFromString(token.symbol + token.name); const bg = `linear-gradient(150deg, hsl(${h} 68% 28%), hsl(${(h + 42) % 360} 62% 16%))`; return (
{token.symbol.length > 4 ? token.symbol.slice(0, 4) : token.symbol}
); } /* ---------- dashboard watchlist card ---------- */ function TokenWatchlist({ onManage }) { const store = useStore(); const prices = useLivePrices(store.tokens); const byId = (id) => store.tokens.find((t) => t.id === id); const watched = (store.user.watchTokens || []).map(byId).filter(Boolean); return (
Markets

Your watchlist

{watched.length === 0 ? (

No tokens yet. Pick the coins you track and watch them live here.

) : (
{watched.map((t) => { const live = prices[t.id] || { price: t.price, change: t.change24h }; const up = live.change >= 0; return (
{t.symbol} {t.name}
{fmtPrice(live.price)} {fmtChange(live.change)}
); })}
)}
); } /* ---------- token picker modal ---------- */ function TokenPicker({ onClose }) { const store = useStore(); const [q, setQ] = useState(""); useEffect(() => { const k = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", k); return () => window.removeEventListener("keydown", k); }, []); const watched = store.user.watchTokens || []; const MAX_WATCH = window.PEST_APP.MAX_WATCH; const full = watched.length >= MAX_WATCH; const ql = q.trim().toLowerCase(); const list = store.tokens.filter( (t) => !ql || t.symbol.toLowerCase().includes(ql) || t.name.toLowerCase().includes(ql) ); const toggle = (t) => { if (!watched.includes(t.id) && full) { toast("Watchlist is full (" + MAX_WATCH + " max)", "err"); return; } store.toggleToken(t.id); }; return ReactDOM.createPortal(
e.stopPropagation()}>
Markets

Manage watchlist

Pick the tokens you want on your dashboard. {watched.length}/{MAX_WATCH} selected · the community's top 6 show on the public board.

setQ(e.target.value)} />
{list.map((t) => { const on = watched.includes(t.id); const blocked = !on && full; return ( ); })} {list.length === 0 &&

No tokens match “{q}”.

}
, document.body ); } /* ---------- public board: top 6 most-watched tokens ---------- */ function topCommunityTokens(store, n) { const mine = new Set(store.user.watchTokens || []); return [...store.tokens] .map((t) => ({ t, score: t.watchers + (mine.has(t.id) ? 1 : 0) })) .sort((a, b) => b.score - a.score) .slice(0, n) .map((x) => x.t); } function GuestTokenStrip({ tokens }) { const store = useStore(); // Guests get the pre-ranked public-board tokens via prop; fall back to the store otherwise. const top = ((tokens && tokens.length) ? tokens : topCommunityTokens(store, 8)).slice(0, 8); const prices = useLivePrices(top); if (!top.length) return null; return (
Most-watched by the Core
{top.map((t) => { const live = prices[t.id] || { price: t.price, change: t.change24h }; const up = live.change >= 0; return (
{t.symbol} {fmtChange(live.change)}
{fmtPrice(live.price)}
); })}
); } Object.assign(window, { PriceEngine, useLivePrices, fmtPrice, fmtChange, TokenTile, TokenWatchlist, TokenPicker, GuestTokenStrip, topCommunityTokens, });