stock-analysis-api / frontend /src /components /PortfolioView.jsx
vjeai's picture
Deploy: all fixes — yfinance candles, ml_signal 2y history, no handoff schemas, sequential report phase
3be03dd
Raw
History Blame Contribute Delete
5 kB
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>
)
}