Spaces:
Sleeping
Sleeping
| import { useState, useEffect, useRef } from 'react' | |
| import anime from 'animejs' | |
| const REC_COLOR = { | |
| 'STRONG BUY': 'var(--buy)', | |
| 'BUY': 'var(--buy)', | |
| 'HOLD': 'var(--hold)', | |
| 'SELL': 'var(--sell)', | |
| 'STRONG SELL':'var(--sell)', | |
| } | |
| const REC_BG = { | |
| 'STRONG BUY': 'rgba(0,232,122,0.12)', | |
| 'BUY': 'rgba(0,232,122,0.08)', | |
| 'HOLD': 'rgba(255,204,68,0.08)', | |
| 'SELL': 'rgba(255,68,102,0.08)', | |
| 'STRONG SELL':'rgba(255,68,102,0.12)', | |
| } | |
| function ScoreBar({ score }) { | |
| const pct = Math.round(((score + 1) / 2) * 100) | |
| const color = score > 0.1 ? 'var(--buy)' : score < -0.1 ? 'var(--sell)' : 'var(--hold)' | |
| return ( | |
| <div style={{ display:'flex', alignItems:'center', gap:'0.5rem', flex:1 }}> | |
| <div style={{ flex:1, height:4, background:'rgba(255,255,255,0.06)', borderRadius:2, overflow:'hidden' }}> | |
| <div style={{ width:`${pct}%`, height:'100%', background:color, borderRadius:2, transition:'width 0.8s ease' }} /> | |
| </div> | |
| <span style={{ fontSize:'0.75rem', color, minWidth:40, textAlign:'right', fontWeight:600 }}> | |
| {score > 0 ? '+' : ''}{score.toFixed(2)} | |
| </span> | |
| </div> | |
| ) | |
| } | |
| export default function PortfolioView({ onAnalyse }) { | |
| const [data, setData] = useState(null) | |
| const [loading, setLoading] = useState(true) | |
| const [error, setError] = useState(null) | |
| const listRef = useRef(null) | |
| useEffect(() => { | |
| fetch('/portfolio') | |
| .then(r => { if (!r.ok) throw new Error('Failed to load portfolio'); return r.json() }) | |
| .then(d => { setData(d); setLoading(false) }) | |
| .catch(e => { setError(e.message); setLoading(false) }) | |
| }, []) | |
| useEffect(() => { | |
| if (!data) return | |
| anime({ | |
| targets: '.holding-row', | |
| translateY: [20, 0], | |
| opacity: [0, 1], | |
| delay: anime.stagger(60), | |
| easing: 'easeOutExpo', | |
| duration: 500, | |
| }) | |
| }, [data]) | |
| if (loading) return ( | |
| <div style={{ textAlign:'center', padding:'3rem', color:'var(--text-muted)' }}> | |
| <div style={{ fontSize:'1.5rem', marginBottom:'0.75rem' }}>📊</div> | |
| Loading your portfolio… | |
| </div> | |
| ) | |
| if (error) return ( | |
| <div style={{ textAlign:'center', padding:'3rem', color:'var(--sell)' }}> | |
| ⚠️ {error} — is the backend running? | |
| </div> | |
| ) | |
| const { holdings, summary } = data | |
| return ( | |
| <div className="portfolio-view"> | |
| {/* Summary bar */} | |
| <div className="portfolio-summary"> | |
| <div className="port-stat"> | |
| <span className="port-stat-val">{summary.total}</span> | |
| <span className="port-stat-label">Holdings</span> | |
| </div> | |
| <div className="port-stat buy"> | |
| <span className="port-stat-val">{summary.buy}</span> | |
| <span className="port-stat-label">Buy signals</span> | |
| </div> | |
| <div className="port-stat hold"> | |
| <span className="port-stat-val">{summary.hold}</span> | |
| <span className="port-stat-label">Hold signals</span> | |
| </div> | |
| <div className="port-stat sell"> | |
| <span className="port-stat-val">{summary.sell}</span> | |
| <span className="port-stat-label">Sell signals</span> | |
| </div> | |
| </div> | |
| {/* Holdings table */} | |
| <div className="card" ref={listRef}> | |
| <div className="holdings-header"> | |
| <span>Ticker</span> | |
| <span>Sector</span> | |
| <span>Weight</span> | |
| <span>Signal</span> | |
| <span>Score</span> | |
| <span>Conf.</span> | |
| <span></span> | |
| </div> | |
| {holdings.map((h) => ( | |
| <div key={h.ticker} className="holding-row" style={{ opacity: 0 }}> | |
| <div className="holding-ticker"> | |
| <span className="hticker">{h.ticker}</span> | |
| <span className="hname">{h.name?.split(' ').slice(0,3).join(' ')}</span> | |
| </div> | |
| <span className="holding-sector">{h.sector}</span> | |
| <span className="holding-weight">{h.pct_of_portfolio}%</span> | |
| <span | |
| className="holding-rec" | |
| style={{ | |
| color: REC_COLOR[h.recommendation] || 'var(--text-muted)', | |
| background: REC_BG[h.recommendation] || 'transparent', | |
| }} | |
| > | |
| {h.recommendation} | |
| </span> | |
| <div className="holding-score"> | |
| <ScoreBar score={h.final_score} /> | |
| </div> | |
| <span className="holding-conf" style={{ color: h.confidence >= 60 ? 'var(--text)' : 'var(--text-muted)' }}> | |
| {h.confidence}% | |
| </span> | |
| <button | |
| className="holding-analyse" | |
| onClick={() => onAnalyse(h.ticker)} | |
| title={`Analyse ${h.ticker}`} | |
| > | |
| → | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| <p className="portfolio-note"> | |
| Signals are quick estimates — click → on any row for a full deep analysis. | |
| </p> | |
| </div> | |
| ) | |
| } | |