jashdoshi77's picture
added dark mode
e85ce30
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: '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> },
]
},
];
// 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}
</>
);
}