import { useState, useEffect } from 'react'; import { ArrowRight } from 'lucide-react'; import { useAppContext } from '../../context/AppContext'; import { fetchNews, type NewsArticle as MarketNewsArticle } from '../../lib/marketApi'; import { SkeletonBlock, SkeletonText } from '../Skeleton'; import { Button } from '../ui/Primitives'; // ─── Portfolio Summary ──────────────────────────────────────────────────────── function PortfolioSummary() { const { botState } = useAppContext(); const positions = botState.positions ?? []; const account = botState.accountSnapshot; const totalValue = account ? account.cash + positions.reduce((s, p) => s + (p.marketValue ?? 0), 0) : positions.reduce((s, p) => s + (p.marketValue ?? 0), 0); const totalPnl = positions.reduce((s, p) => s + (p.unrealizedPnl ?? 0), 0); const pnlPos = totalPnl >= 0; const fmt$ = (n: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n); return (
{/* Header */}
Portfolio
{/* Total value */}
{fmt$(totalValue)}
{pnlPos ? '+' : ''}{fmt$(totalPnl)} unrealized
{/* Column headers */}
{['Symbol','Price','Change','Value'].map(h => ( {h} ))}
{/* Rows */} {positions.length === 0 ? (
No open positions
Filled Trade Plans and manual entries will appear here.
) : (
{positions.slice(0, 6).map(pos => { const pct = pos.unrealizedPnlPercent ?? 0; const pos_ = pct >= 0; return (
{pos.symbol} {pos.currentPrice?.toFixed(2) ?? '—'} {pos_ ? '+' : ''}{pct.toFixed(2)}% {fmt$(pos.marketValue ?? 0)}
); })}
)}
); } // ─── News Feed ──────────────────────────────────────────────────────────────── type NewsArticle = MarketNewsArticle; function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const m = Math.floor(diff / 60_000); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; } function NewsCard({ article }: { article: NewsArticle }) { const img = article.images?.[0]?.url; return ( (e.currentTarget.style.background = 'var(--muted)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} > {img && ( { (e.target as HTMLImageElement).style.display = 'none'; }} /> )}
{article.headline}
{article.source} · {timeAgo(article.created_at)}
); } function NewsFeedSkeleton() { return (
{[0, 1, 2].map((i) => (
))}
); } function NewsFeed() { const { activeSymbol } = useAppContext(); const [news, setNews] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!activeSymbol) { setNews([]); return; } let cancelled = false; setLoading(true); setError(null); // Use the authenticated marketApi helper so the request carries the // platform Bearer token; the raw `fetch()` we used before was hitting // requireAuth and 401-ing on every render. fetchNews(activeSymbol, 8) .then(list => { if (!cancelled) setNews(list); }) .catch(err => { if (!cancelled) setError(err?.message ?? 'Failed to load news'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [activeSymbol]); return (
Latest News
{loading && } {!loading && error && (
{error}
)} {!loading && !error && news.length === 0 && (
{activeSymbol ? `No news for ${activeSymbol}` : 'No symbol selected'}
{activeSymbol ? 'Try another ticker or check back after the next market update.' : 'Search a ticker to load market news.'}
)} {!loading && news.map((a, i) => )}
); } // ─── Right Panel container ──────────────────────────────────────────────────── export function RightPanel() { return ( ); }