// Watchlist.jsx — server-backed via /api/watchlist. const WatchlistPage = () => { const { useState, useEffect } = React; const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ symbol: '', notes: '', sector: '', priceTarget: '', alertPrice: '', priority: 'medium' }); const [editId, setEditId] = useState(null); const [filter, setFilter] = useState('all'); // Initial load. useEffect(() => { let cancelled = false; setLoading(true); setLoadError(null); window.api.listWatchlist() .then(rows => { if (!cancelled) { setItems(rows); setLoading(false); } }) .catch(err => { if (!cancelled) { setLoadError(err.message || String(err)); setLoading(false); } }); return () => { cancelled = true; }; }, []); const PRIORITY_COLORS = { high: '#f04060', medium: '#ffb83f', low: '#5588ff' }; const SECTORS = ['Tech','AI','Index','Energy','Financials','Healthcare','Consumer','Industrials','Other']; const resetForm = () => setForm({ symbol: '', notes: '', sector: '', priceTarget: '', alertPrice: '', priority: 'medium' }); // Build the payload that the API expects. Empty strings get sent through; // the server normalizes/null-ifies them. const buildPayload = (raw) => ({ symbol: raw.symbol.toUpperCase().trim(), notes: raw.notes || '', sector: raw.sector || '', priority: raw.priority || 'medium', priceTarget: raw.priceTarget === '' || raw.priceTarget == null ? null : Number(raw.priceTarget), alertPrice: raw.alertPrice === '' || raw.alertPrice == null ? null : Number(raw.alertPrice), }); const save = async () => { if (!form.symbol.trim() || saving) return; setSaving(true); try { const payload = buildPayload(form); if (editId) { const updated = await window.api.updateWatchlistItem(editId, payload); setItems(its => its.map(i => i.id === editId ? updated : i)); setEditId(null); } else { const created = await window.api.addWatchlistItem(payload); setItems(its => [created, ...its]); } resetForm(); } catch (err) { // eslint-disable-next-line no-alert alert('Failed to save: ' + (err.message || 'unknown error')); } finally { setSaving(false); } }; const del = async (id) => { // Optimistic delete with rollback on failure. const snapshot = items; setItems(its => its.filter(i => i.id !== id)); try { await window.api.deleteWatchlistItem(id); } catch (err) { setItems(snapshot); // eslint-disable-next-line no-alert alert('Failed to delete: ' + (err.message || 'unknown error')); } }; const startEdit = (item) => { setEditId(item.id); setForm({ symbol: item.symbol || '', notes: item.notes || '', sector: item.sector || '', priceTarget: item.priceTarget == null ? '' : String(item.priceTarget), alertPrice: item.alertPrice == null ? '' : String(item.alertPrice), priority: item.priority || 'medium', }); }; const filtered = filter === 'all' ? items : items.filter(i => i.priority === filter); const inp = (extra={}) => ({ background: 'var(--surface3)', border: '1px solid var(--border)', borderRadius: '6px', color: 'var(--text)', padding: '8px 12px', fontSize: '13px', outline: 'none', fontFamily: 'Space Grotesk, sans-serif', ...extra }); return React.createElement('div', { style: { padding: '28px 32px', maxWidth: '1100px' } }, [ // Header React.createElement('div', { key: 'hdr', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' } }, [ React.createElement('div', { key: 'l' }, [ React.createElement('h1', { key: 'h', style: { fontSize: '22px', fontWeight: 700, color: 'var(--text)', margin: 0 } }, 'Watchlist'), React.createElement('p', { key: 's', style: { fontSize: '13px', color: 'var(--muted)', margin: '4px 0 0' } }, loading ? 'Loading…' : `${items.length} symbols tracked`) ]), React.createElement('div', { key: 'filters', style: { display: 'flex', gap: '4px', background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '8px', padding: '4px' } }, [['all','All'],['high','High'],['medium','Med'],['low','Low']].map(([id, lbl]) => React.createElement('button', { key: id, onClick: () => setFilter(id), style: { padding: '5px 12px', fontSize: '12px', fontWeight: filter===id ? 600 : 400, borderRadius: '6px', border: 'none', cursor: 'pointer', fontFamily: 'Space Grotesk, sans-serif', background: filter===id ? (id==='all'?'var(--border)': PRIORITY_COLORS[id]) : 'transparent', color: filter===id ? (id==='all'?'var(--text)':'#fff') : 'var(--muted)', transition: 'all 0.15s' } }, lbl) ) ) ]), // Load error banner loadError && React.createElement('div', { key: 'err', style: { background: 'rgba(240,64,96,0.08)', border: '1px solid rgba(240,64,96,0.25)', color: '#f04060', padding: '12px 16px', borderRadius: '8px', fontSize: '13px', marginBottom: '20px' } }, 'Could not load watchlist: ' + loadError), // Add / Edit form React.createElement('div', { key: 'form', style: { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '10px', padding: '18px 22px', marginBottom: '20px' } }, [ React.createElement('div', { key: 'title', style: { fontSize: '12px', color: 'var(--muted)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.07em', marginBottom: '14px' } }, editId ? 'Edit Symbol' : 'Add to Watchlist'), React.createElement('div', { key: 'row1', style: { display: 'grid', gridTemplateColumns: '120px 140px 140px 140px 1fr', gap: '10px', marginBottom: '10px', alignItems: 'end' } }, [ React.createElement('div', { key: 'sym' }, [ React.createElement('label', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', display: 'block', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.07em' } }, 'Symbol *'), React.createElement('input', { value: form.symbol, onChange: e => setForm(f=>({...f, symbol: e.target.value.toUpperCase()})), placeholder: 'AAPL', style: { ...inp(), width: '100%', boxSizing: 'border-box', fontFamily: 'JetBrains Mono, monospace', letterSpacing: '0.04em' } }) ]), React.createElement('div', { key: 'sec' }, [ React.createElement('label', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', display: 'block', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.07em' } }, 'Sector'), React.createElement('select', { value: form.sector, onChange: e => setForm(f=>({...f,sector:e.target.value})), style: { ...inp(), width: '100%', boxSizing: 'border-box' } }, [ React.createElement('option', { key: '', value: '' }, '—'), ...SECTORS.map(s => React.createElement('option', { key: s, value: s }, s)) ]) ]), React.createElement('div', { key: 'pt' }, [ React.createElement('label', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', display: 'block', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.07em' } }, 'Price Target'), React.createElement('input', { type: 'number', step: '0.01', value: form.priceTarget, onChange: e => setForm(f=>({...f,priceTarget:e.target.value})), placeholder: '200.00', style: { ...inp(), width: '100%', boxSizing: 'border-box' } }) ]), React.createElement('div', { key: 'pri' }, [ React.createElement('label', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', display: 'block', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.07em' } }, 'Priority'), React.createElement('div', { key: 'btns', style: { display: 'flex', background: 'var(--surface3)', border: '1px solid var(--border)', borderRadius: '6px', padding: '3px', gap: '3px' } }, ['high','medium','low'].map(p => React.createElement('button', { key: p, onClick: () => setForm(f=>({...f,priority:p})), type: 'button', style: { flex: 1, padding: '5px 4px', fontSize: '11px', border: 'none', borderRadius: '4px', cursor: 'pointer', fontFamily: 'Space Grotesk, sans-serif', background: form.priority===p ? PRIORITY_COLORS[p] : 'transparent', color: form.priority===p ? '#fff' : 'var(--muted)', fontWeight: form.priority===p ? 600 : 400, textTransform: 'capitalize' } }, p)) ) ]), React.createElement('div', { key: 'notes' }, [ React.createElement('label', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', display: 'block', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.07em' } }, 'Setup Notes'), React.createElement('input', { value: form.notes, onChange: e => setForm(f=>({...f,notes:e.target.value})), placeholder: 'Why watching this? Entry conditions…', style: { ...inp(), width: '100%', boxSizing: 'border-box' } }) ]), ]), React.createElement('div', { key: 'actions', style: { display: 'flex', gap: '8px' } }, [ React.createElement('button', { onClick: save, disabled: saving, style: { background: saving ? '#3a5acc' : '#5588ff', border: 'none', borderRadius: '7px', color: '#fff', fontSize: '13px', fontWeight: 600, cursor: saving ? 'wait' : 'pointer', padding: '8px 20px', fontFamily: 'Space Grotesk, sans-serif' } }, saving ? 'Saving…' : (editId ? 'Save Changes' : '+ Add')), editId && React.createElement('button', { key: 'cancel', onClick: () => { setEditId(null); resetForm(); }, style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '7px', color: 'var(--text2)', fontSize: '13px', cursor: 'pointer', padding: '8px 16px', fontFamily: 'Space Grotesk, sans-serif' } }, 'Cancel') ]) ]), // List loading ? React.createElement('div', { key: 'loading', style: { textAlign: 'center', padding: '60px', color: 'var(--muted)' } }, 'Loading…') : filtered.length === 0 ? React.createElement('div', { key: 'empty', style: { textAlign: 'center', padding: '60px', color: 'var(--muted)' } }, 'No symbols yet — add your first one above') : React.createElement('div', { key: 'list', style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, filtered.map(item => React.createElement('div', { key: item.id, style: { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '10px', padding: '14px 18px', display: 'flex', alignItems: 'center', gap: '16px', transition: 'border-color 0.15s' }, onMouseEnter: e => e.currentTarget.style.borderColor = '#283060', onMouseLeave: e => e.currentTarget.style.borderColor = 'var(--border)' }, [ // Priority dot React.createElement('div', { key: 'dot', style: { width: '8px', height: '8px', borderRadius: '50%', background: PRIORITY_COLORS[item.priority] || 'var(--muted)', flexShrink: 0, boxShadow: `0 0 6px ${PRIORITY_COLORS[item.priority] || 'var(--muted)'}55` } }), // Symbol React.createElement('span', { key: 'sym', style: { fontFamily: 'JetBrains Mono, monospace', fontWeight: 700, fontSize: '15px', color: 'var(--text)', letterSpacing: '0.04em', width: '80px' } }, item.symbol), // Sector item.sector && React.createElement(Badge, { key: 'sec' }, item.sector), // Notes React.createElement('span', { key: 'notes', style: { flex: 1, fontSize: '13px', color: 'var(--muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.notes || 'No notes'), // Price target item.priceTarget && React.createElement('div', { key: 'pt', style: { display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0 } }, [ React.createElement('span', { key: 'l', style: { fontSize: '11px', color: 'var(--muted)' } }, 'Target'), React.createElement('span', { key: 'v', style: { fontFamily: 'JetBrains Mono, monospace', fontSize: '13px', color: '#00d27a' } }, `$${parseFloat(item.priceTarget).toFixed(2)}`) ]), // Date added React.createElement('span', { key: 'date', style: { fontSize: '11px', color: 'var(--dim)', flexShrink: 0 } }, item.addedAt), // Actions React.createElement('div', { key: 'acts', style: { display: 'flex', gap: '6px', flexShrink: 0 } }, [ React.createElement('button', { key: 'e', onClick: () => startEdit(item), style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '5px', color: 'var(--text2)', fontSize: '12px', cursor: 'pointer', padding: '4px 10px' } }, 'Edit'), React.createElement('button', { key: 'd', onClick: () => del(item.id), style: { background: 'rgba(240,64,96,0.08)', border: '1px solid rgba(240,64,96,0.15)', borderRadius: '5px', color: '#f04060', fontSize: '12px', cursor: 'pointer', padding: '4px 8px' } }, '×') ]) ]) ) ) ]); }; Object.assign(window, { WatchlistPage });