142 lines
5.6 KiB
TypeScript
142 lines
5.6 KiB
TypeScript
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
|
import { usePortfolio } from '../hooks/usePortfolio';
|
|
import { useWebSocket } from '../hooks/useWebSocket';
|
|
|
|
const navItems = [
|
|
{ to: '/portfolio', label: 'Portfolio', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
|
|
{ to: '/trades', label: 'Trades', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2' },
|
|
{ to: '/strategies', label: 'Strategies', icon: 'M13 10V3L4 14h7v7l9-11h-7z' },
|
|
{ to: '/news', label: 'News', icon: 'M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2' },
|
|
{ to: '/backtest', label: 'Backtest', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
|
|
{ to: '/meet-kevin', label: 'Meet Kevin', icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z' },
|
|
];
|
|
|
|
export function Layout() {
|
|
const navigate = useNavigate();
|
|
const { data: portfolio } = usePortfolio();
|
|
const { lastEvent } = useWebSocket();
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
navigate('/login');
|
|
};
|
|
|
|
const portfolioValue = portfolio?.total_value ?? 0;
|
|
const dailyPnl = portfolio?.daily_pnl ?? 0;
|
|
const isActive = portfolio?.trading_active ?? false;
|
|
|
|
return (
|
|
<div className="flex h-screen bg-slate-900">
|
|
{/* Sidebar */}
|
|
<aside className="w-64 bg-slate-800 border-r border-slate-700 flex flex-col">
|
|
<div className="p-6 border-b border-slate-700">
|
|
<h1 className="text-xl font-bold text-white">Trading Bot</h1>
|
|
<p className="text-xs text-slate-400 mt-1">Automated Trading Dashboard</p>
|
|
</div>
|
|
|
|
<nav className="flex-1 p-4 space-y-1">
|
|
{navItems.map((item) => (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
className={({ isActive }) =>
|
|
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors ${
|
|
isActive
|
|
? 'bg-blue-600/20 text-blue-400'
|
|
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
|
|
}`
|
|
}
|
|
>
|
|
<svg
|
|
className="w-5 h-5 flex-shrink-0"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d={item.icon}
|
|
/>
|
|
</svg>
|
|
{item.label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="p-4 border-t border-slate-700">
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-400 hover:text-white hover:bg-slate-700 rounded-lg transition-colors"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
/>
|
|
</svg>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main content */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* Top bar */}
|
|
<header className="h-16 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-6">
|
|
<div className="flex items-center gap-6">
|
|
<div>
|
|
<span className="text-sm text-slate-400">Portfolio Value</span>
|
|
<p className="text-lg font-semibold text-white">
|
|
${portfolioValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-slate-400">Daily P&L</span>
|
|
<p
|
|
className={`text-lg font-semibold ${
|
|
dailyPnl >= 0 ? 'text-green-400' : 'text-red-400'
|
|
}`}
|
|
>
|
|
{dailyPnl >= 0 ? '+' : ''}
|
|
${dailyPnl.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{lastEvent && (
|
|
<span className="text-xs text-slate-400 bg-slate-700 px-2 py-1 rounded">
|
|
{lastEvent.type}
|
|
</span>
|
|
)}
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`w-2.5 h-2.5 rounded-full ${
|
|
isActive ? 'bg-green-400 animate-pulse' : 'bg-red-400'
|
|
}`}
|
|
/>
|
|
<span className="text-sm text-slate-300">
|
|
{isActive ? 'Active' : 'Paused'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page content */}
|
|
<main className="flex-1 overflow-auto p-6">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|