/* ═══════════════════════════════════════════════════════════════════ MY HOLDINGS · owner-entered positions (qty + avg cost) Powers the "my avg / qty / value / P&L" personalization on every page. API-independent — works even though Bybit's private API is geo-blocked. ═══════════════════════════════════════════════════════════════════ */ function HoldingsModal({ open, onClose, coins, onSaved }) { const [rows, setRows] = React.useState({}); /* sym -> {qty, avg} */ const [saving, setSaving] = React.useState(false); const [query, setQuery] = React.useState(''); /* load existing holdings when opened */ React.useEffect(() => { if (!open) return; setQuery(''); ST_fetch('/holdings').then(d => { const map = {}; (d.holdings || []).forEach(h => { map[h.symbol] = { qty: String(h.qty), avg: String(h.avg_cost) }; }); setRows(map); }).catch(() => setRows({})); }, [open]); if (!open) return null; const list = (coins || ST_COINS).filter(c => !query || c.sym.toLowerCase().includes(query.toLowerCase()) || (c.name||'').toLowerCase().includes(query.toLowerCase()) ); /* held coins first */ list.sort((a, b) => (rows[b.sym] ? 1 : 0) - (rows[a.sym] ? 1 : 0)); const setRow = (sym, field, val) => setRows(prev => ({ ...prev, [sym]: { ...(prev[sym] || { qty: '', avg: '' }), [field]: val } })); const totalValue = list.reduce((s, c) => { const r = rows[c.sym]; const q = parseFloat(r?.qty || 0) || 0; return s + q * (c.price || 0); }, 0); const save = async () => { setSaving(true); const entries = Object.entries(rows); for (const [sym, r] of entries) { const qty = parseFloat(r.qty || 0) || 0; const avg = parseFloat(r.avg || 0) || 0; try { await fetch(window.ST_API + '/holdings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: sym, qty, avg_cost: avg }), }); } catch (_) {} } setSaving(false); if (onSaved) onSaved(); onClose(); }; return (
e.stopPropagation()} style={{ width: 620, maxHeight: '82vh', display: 'flex', flexDirection: 'column', background: ST.bg2, border: `1px solid ${ST.borderHi}`, borderRadius: 12, boxShadow: '0 30px 80px -30px rgba(0,0,0,0.9)', overflow: 'hidden', }}> {/* header */}
My Holdings
enter your real qty + avg cost · shown on every page
TOTAL VALUE
{totalValue > 0 ? `$${totalValue.toLocaleString('en-US', { maximumFractionDigits: 0 })}` : '$0'}
{/* search */}
setQuery(e.target.value)} placeholder="Search coin…" className="mono" style={{ width: '100%', padding: '8px 11px', borderRadius: 7, fontSize: 12, background: ST.panel, border: `1px solid ${ST.border}`, color: ST.ink, outline: 'none' }} />
{/* column heads */}
{['COIN', 'QUANTITY', 'AVG COST $', 'VALUE / P&L'].map((h, i) => ( {h} ))}
{/* rows */}
{list.map(c => { const r = rows[c.sym] || { qty: '', avg: '' }; const qty = parseFloat(r.qty || 0) || 0; const avg = parseFloat(r.avg || 0) || 0; const val = qty * (c.price || 0); const pnl = (qty > 0 && avg > 0 && c.price) ? ((c.price - avg) / avg * 100) : null; const held = qty > 0; return (
{c.sym}
${ST_fmt.price(c.price)}
setRow(c.sym, 'qty', e.target.value)} placeholder="0" inputMode="decimal" className="num" style={inputStyle} /> setRow(c.sym, 'avg', e.target.value)} placeholder="0.00" inputMode="decimal" className="num" style={inputStyle} />
{held ? '$' + ST_fmt.price(val) : '—'}
{pnl != null &&
= 0 ? ST.green : ST.red }}> {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
}
); })}
{/* footer */}
); } const inputStyle = { width: '100%', padding: '7px 9px', borderRadius: 6, fontSize: 12, textAlign: 'right', background: '#0B0D14', border: '1px solid #1E2130', color: '#ECEFF6', outline: 'none', }; window.HoldingsModal = HoldingsModal;