quanthedge / frontend /src /components /Sidebar.tsx
jashdoshi77's picture
added live trading
aa7f6ee
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useState, useEffect } from 'react';
interface SidebarProps {
onExpandChange?: (expanded: boolean) => void;
}
export default function Sidebar({ onExpandChange }: SidebarProps) {
const navigate = useNavigate();
const location = useLocation();
const user = JSON.parse(localStorage.getItem('qh_user') || 'null');
const [drawerOpen, setDrawerOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [isDark, setIsDark] = useState(() => {
return localStorage.getItem('qh_theme') === 'dark' || document.documentElement.getAttribute('data-theme') === 'dark';
});
useEffect(() => {
const check = () => setIsMobile(window.innerWidth <= 768);
check();
window.addEventListener('resize', check);
return () => window.removeEventListener('resize', check);
}, []);
// Apply theme on mount and when toggled
useEffect(() => {
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
localStorage.setItem('qh_theme', isDark ? 'dark' : 'light');
}, [isDark]);
// Close drawer on route change
useEffect(() => { setDrawerOpen(false); }, [location.pathname]);
const handleLogout = () => {
localStorage.removeItem('qh_token');
localStorage.removeItem('qh_user');
navigate('/login');
};
const toggleTheme = () => setIsDark(prev => !prev);
const themeIcon = isDark ? (
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" /></svg>
) : (
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /></svg>
);
const allLinks = [
{
section: 'Overview',
items: [
{ to: '/dashboard', label: 'Dashboard', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" /></svg> },
{ to: '/holdings', label: 'Holdings', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clipRule="evenodd" /></svg> },
]
},
{
section: 'Research',
items: [
{ to: '/market', label: 'Markets', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z" clipRule="evenodd" /></svg> },
{ to: '/factors', label: 'Factor Analysis', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zm6-4a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zm6-3a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" /></svg> },
{ to: '/sentiment', label: 'Sentiment', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z" clipRule="evenodd" /></svg> },
{ to: '/research', label: 'Research', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" /></svg> },
{ to: '/calendar', label: 'Calendar', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg> },
]
},
{
section: 'Quantitative',
items: [
{ to: '/strategies', label: 'Strategy Builder', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" /></svg> },
{ to: '/backtests', label: 'Backtests', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" /></svg> },
]
},
{
section: 'Analytics',
items: [
{ to: '/portfolio-health', label: 'Health Score', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clipRule="evenodd" /></svg> },
{ to: '/bias-detector', label: 'Bias Detector', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" /><path fillRule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg> },
{ to: '/crisis-replay', label: 'Crisis Replay', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M12.395 2.553a1 1 0 00-1.45-.385c-.345.23-.614.558-.822.88-.214.33-.403.713-.57 1.116-.334.804-.614 1.768-.84 2.734a31.365 31.365 0 00-.613 3.58 2.64 2.64 0 01-.945-1.067c-.328-.68-.398-1.534-.398-2.654A1 1 0 005.05 6.05 6.981 6.981 0 003 11a7 7 0 1011.95-4.95c-.592-.591-.98-.985-1.348-1.467-.363-.476-.724-1.063-1.207-2.03zM12.12 15.12A3 3 0 017 13s.879.5 2.5.5c0-1 .5-4 1.25-4.5.5 1 .786 1.293 1.371 1.879A2.99 2.99 0 0113 13a2.99 2.99 0 01-.879 2.121z" clipRule="evenodd" /></svg> },
{ to: '/portfolio-dna', label: 'Portfolio DNA', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 3.5a1.5 1.5 0 013 0V4a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-.5a1.5 1.5 0 000 3h.5a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-.5a1.5 1.5 0 00-3 0v.5a1 1 0 01-1 1H6a1 1 0 01-1-1v-3a1 1 0 00-1-1h-.5a1.5 1.5 0 010-3H4a1 1 0 001-1V6a1 1 0 011-1h3a1 1 0 001-1v-.5z" /></svg> },
]
},
{
section: 'Portfolio',
items: [
{ to: '/portfolio', label: 'Optimization', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" /><path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" /></svg> },
{ to: '/marketplace', label: 'Marketplace', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" /></svg> },
{ to: '/copilot', label: 'HedgeAI', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6.672 1.911a1 1 0 10-1.932.518l.259.966a1 1 0 001.932-.518l-.26-.966zM2.429 4.74a1 1 0 10-.517 1.932l.966.259a1 1 0 00.517-1.932l-.966-.26zm8.814-.569a1 1 0 00-1.415-1.414l-.707.707a1 1 0 101.415 1.415l.707-.708zm-7.071 7.072l.707-.707A1 1 0 003.465 9.12l-.708.707a1 1 0 001.415 1.415zm3.2-5.171a1 1 0 00-1.3 1.3l4 10a1 1 0 001.823.075l1.38-2.759 3.018 3.02a1 1 0 001.414-1.415l-3.019-3.02 2.76-1.379a1 1 0 00-.076-1.822l-10-4z" clipRule="evenodd" /></svg> },
]
},
{
section: 'AI Engine',
items: [
{ to: '/pattern-intelligence', label: 'Patterns', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg> },
{ to: '/pinescript-lab', label: 'Pine Script', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" clipRule="evenodd" /></svg> },
{ to: '/paper-trading', label: 'Paper Trading', icon: <svg viewBox="0 0 20 20" fill="currentColor"><path d="M4 4a2 2 0 00-2 2v1h16V6a2 2 0 00-2-2H4z" /><path fillRule="evenodd" d="M18 9H2v5a2 2 0 002 2h12a2 2 0 002-2V9zM4 13a1 1 0 011-1h1a1 1 0 110 2H5a1 1 0 01-1-1zm5-1a1 1 0 100 2h1a1 1 0 100-2H9z" clipRule="evenodd" /></svg> },
]
},
];
// Primary nav items for mobile bottom bar (5 key items)
const mobileNavItems = [
allLinks[0].items[0], // Dashboard
allLinks[1].items[0], // Markets
allLinks[0].items[1], // Holdings
allLinks[1].items[1], // Factor Analysis
];
// ── Desktop Sidebar ──────────────────────────────────────────────
const desktopSidebar = (
<aside
className={`sidebar${isExpanded ? ' sidebar-expanded' : ''}`}
onMouseEnter={() => { setIsExpanded(true); onExpandChange?.(true); }}
onMouseLeave={() => { setIsExpanded(false); onExpandChange?.(false); }}
> <div className="sidebar-brand">
<NavLink to="/dashboard" className="sidebar-logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#005241" />
<path d="M8 22 L12 14 L16 18 L20 10 L24 16" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</svg>
<span className="sidebar-brand-text">
Quant<em>Hedge</em>
</span>
</NavLink>
</div>
<nav className="sidebar-nav">
{allLinks.map((group) => (
<div key={group.section} className="sidebar-section">
<div className="sidebar-section-label">{group.section}</div>
{group.items.map((link) => (
<NavLink
key={link.to}
to={link.to}
className={({ isActive }) => `sidebar-link ${isActive ? 'active' : ''}`}
>
<span className="sidebar-icon">{link.icon}</span>
<span className="sidebar-label">{link.label}</span>
</NavLink>
))}
</div>
))}
</nav>
<div className="sidebar-footer">
{user && (
<div className="sidebar-user">
<div className="sidebar-avatar">
{(user.username || 'U').charAt(0).toUpperCase()}
</div>
<div className="sidebar-user-info">
<div className="sidebar-user-name">{user.full_name || user.username}</div>
<div className="sidebar-user-email">{user.email}</div>
</div>
</div>
)}
<button className="sidebar-link" onClick={toggleTheme}>
<span className="sidebar-icon">{themeIcon}</span>
<span className="sidebar-label">{isDark ? 'Light Mode' : 'Dark Mode'}</span>
</button>
<button className="sidebar-link sidebar-logout" onClick={handleLogout}>
<span className="sidebar-icon">
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd" /></svg>
</span>
<span className="sidebar-label">Log Out</span>
</button>
</div>
</aside>
);
// ── Mobile Bottom Tab Bar + Drawer ───────────────────────────────
const mobileNav = (
<>
<nav className="mobile-nav">
<div className="mobile-nav-items">
{mobileNavItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) => `mobile-nav-item ${isActive ? 'active' : ''}`}
>
{item.icon}
<span>{item.label}</span>
</NavLink>
))}
{/* More button */}
<button
className={`mobile-nav-item ${drawerOpen ? 'active' : ''}`}
onClick={() => setDrawerOpen(!drawerOpen)}
>
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20">
<path fillRule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
</svg>
<span>More</span>
</button>
</div>
</nav>
{/* Slide-up drawer with all nav items */}
{drawerOpen && (
<>
<div className="mobile-drawer-overlay" onClick={() => setDrawerOpen(false)} />
<div className="mobile-drawer">
<div className="mobile-drawer-handle" />
{allLinks.map((group) => (
<div key={group.section} className="sidebar-section">
<div className="sidebar-section-label">{group.section}</div>
{group.items.map((link) => (
<NavLink
key={link.to}
to={link.to}
className={({ isActive }) => `sidebar-link ${isActive ? 'active' : ''}`}
onClick={() => setDrawerOpen(false)}
>
<span className="sidebar-icon">{link.icon}</span>
<span className="sidebar-label">{link.label}</span>
</NavLink>
))}
</div>
))}
{/* User + Logout in drawer */}
<div style={{ borderTop: '1px solid var(--border-subtle)', marginTop: '0.5rem', paddingTop: '0.75rem' }}>
{user && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.5rem 1rem', marginBottom: '0.5rem' }}>
<div className="sidebar-avatar">
{(user.username || 'U').charAt(0).toUpperCase()}
</div>
<div>
<div style={{ fontSize: '0.85rem', fontWeight: 600 }}>{user.full_name || user.username}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>{user.email}</div>
</div>
</div>
)}
<button className="sidebar-link" onClick={toggleTheme} style={{ width: '100%' }}>
<span className="sidebar-icon">{themeIcon}</span>
<span className="sidebar-label">{isDark ? 'Light Mode' : 'Dark Mode'}</span>
</button>
<button className="sidebar-link sidebar-logout" onClick={handleLogout} style={{ width: '100%' }}>
<span className="sidebar-icon">
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd" /></svg>
</span>
<span className="sidebar-label">Log Out</span>
</button>
</div>
</div>
</>
)}
</>
);
return (
<>
{isMobile ? mobileNav : desktopSidebar}
</>
);
}