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 */}
{/* 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 (
{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 (
);
}