Spaces:
Running
Running
Commit ·
0366c3e
1
Parent(s): 702ea51
changed the sidebar animation and added holdings in the analysis section
Browse files- frontend/src/App.tsx +5 -2
- frontend/src/components/Sidebar.tsx +120 -10
- frontend/src/index.css +391 -11
- frontend/src/pages/Dashboard.tsx +1 -1
- frontend/src/pages/FactorAnalysis.tsx +1 -1
- frontend/src/pages/Landing.tsx +6 -6
- frontend/src/pages/Login.tsx +3 -3
- frontend/src/pages/MarketExplorer.tsx +3 -3
- frontend/src/pages/PortfolioAnalysis.tsx +40 -1
- frontend/src/pages/Register.tsx +3 -3
frontend/src/App.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
|
|
| 2 |
import Sidebar from './components/Sidebar';
|
| 3 |
import Landing from './pages/Landing';
|
| 4 |
import Login from './pages/Login';
|
|
@@ -23,10 +24,12 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
| 23 |
}
|
| 24 |
|
| 25 |
function AppLayout({ children }: { children: React.ReactNode }) {
|
|
|
|
|
|
|
| 26 |
return (
|
| 27 |
<div className="app-layout">
|
| 28 |
-
<Sidebar />
|
| 29 |
-
<main className="app-main">
|
| 30 |
{children}
|
| 31 |
</main>
|
| 32 |
</div>
|
|
|
|
| 1 |
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
| 2 |
+
import { useState } from 'react';
|
| 3 |
import Sidebar from './components/Sidebar';
|
| 4 |
import Landing from './pages/Landing';
|
| 5 |
import Login from './pages/Login';
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
function AppLayout({ children }: { children: React.ReactNode }) {
|
| 27 |
+
const [sidebarExpanded, setSidebarExpanded] = useState(false);
|
| 28 |
+
|
| 29 |
return (
|
| 30 |
<div className="app-layout">
|
| 31 |
+
<Sidebar onExpandChange={setSidebarExpanded} />
|
| 32 |
+
<main className="app-main" style={{ marginLeft: sidebarExpanded ? 260 : 68 }}>
|
| 33 |
{children}
|
| 34 |
</main>
|
| 35 |
</div>
|
frontend/src/components/Sidebar.tsx
CHANGED
|
@@ -1,8 +1,27 @@
|
|
| 1 |
-
import { NavLink, useNavigate } from 'react-router-dom';
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
const navigate = useNavigate();
|
|
|
|
| 5 |
const user = JSON.parse(localStorage.getItem('qh_user') || 'null');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const handleLogout = () => {
|
| 8 |
localStorage.removeItem('qh_token');
|
|
@@ -10,7 +29,7 @@ export default function Sidebar() {
|
|
| 10 |
navigate('/login');
|
| 11 |
};
|
| 12 |
|
| 13 |
-
const
|
| 14 |
{
|
| 15 |
section: 'Overview',
|
| 16 |
items: [
|
|
@@ -44,10 +63,21 @@ export default function Sidebar() {
|
|
| 44 |
},
|
| 45 |
];
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
<NavLink to="/dashboard" className="sidebar-logo">
|
| 52 |
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
| 53 |
<rect width="32" height="32" rx="8" fill="#005241"/>
|
|
@@ -59,9 +89,8 @@ export default function Sidebar() {
|
|
| 59 |
</NavLink>
|
| 60 |
</div>
|
| 61 |
|
| 62 |
-
{/* Navigation */}
|
| 63 |
<nav className="sidebar-nav">
|
| 64 |
-
{
|
| 65 |
<div key={group.section} className="sidebar-section">
|
| 66 |
<div className="sidebar-section-label">{group.section}</div>
|
| 67 |
{group.items.map((link) => (
|
|
@@ -78,7 +107,6 @@ export default function Sidebar() {
|
|
| 78 |
))}
|
| 79 |
</nav>
|
| 80 |
|
| 81 |
-
{/* Bottom user section */}
|
| 82 |
<div className="sidebar-footer">
|
| 83 |
{user && (
|
| 84 |
<div className="sidebar-user">
|
|
@@ -100,4 +128,86 @@ export default function Sidebar() {
|
|
| 100 |
</div>
|
| 101 |
</aside>
|
| 102 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
|
|
|
| 1 |
+
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
|
| 2 |
+
import { useState, useEffect } from 'react';
|
| 3 |
|
| 4 |
+
interface SidebarProps {
|
| 5 |
+
onExpandChange?: (expanded: boolean) => void;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function Sidebar({ onExpandChange }: SidebarProps) {
|
| 9 |
const navigate = useNavigate();
|
| 10 |
+
const location = useLocation();
|
| 11 |
const user = JSON.parse(localStorage.getItem('qh_user') || 'null');
|
| 12 |
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
| 13 |
+
const [isMobile, setIsMobile] = useState(false);
|
| 14 |
+
const [isExpanded, setIsExpanded] = useState(false);
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
const check = () => setIsMobile(window.innerWidth <= 768);
|
| 18 |
+
check();
|
| 19 |
+
window.addEventListener('resize', check);
|
| 20 |
+
return () => window.removeEventListener('resize', check);
|
| 21 |
+
}, []);
|
| 22 |
+
|
| 23 |
+
// Close drawer on route change
|
| 24 |
+
useEffect(() => { setDrawerOpen(false); }, [location.pathname]);
|
| 25 |
|
| 26 |
const handleLogout = () => {
|
| 27 |
localStorage.removeItem('qh_token');
|
|
|
|
| 29 |
navigate('/login');
|
| 30 |
};
|
| 31 |
|
| 32 |
+
const allLinks = [
|
| 33 |
{
|
| 34 |
section: 'Overview',
|
| 35 |
items: [
|
|
|
|
| 63 |
},
|
| 64 |
];
|
| 65 |
|
| 66 |
+
// Primary nav items for mobile bottom bar (5 key items)
|
| 67 |
+
const mobileNavItems = [
|
| 68 |
+
allLinks[0].items[0], // Dashboard
|
| 69 |
+
allLinks[1].items[0], // Markets
|
| 70 |
+
allLinks[0].items[1], // Holdings
|
| 71 |
+
allLinks[1].items[1], // Factor Analysis
|
| 72 |
+
];
|
| 73 |
+
|
| 74 |
+
// ── Desktop Sidebar ──────────────────────────────────────────────
|
| 75 |
+
const desktopSidebar = (
|
| 76 |
+
<aside
|
| 77 |
+
className={`sidebar${isExpanded ? ' sidebar-expanded' : ''}`}
|
| 78 |
+
onMouseEnter={() => { setIsExpanded(true); onExpandChange?.(true); }}
|
| 79 |
+
onMouseLeave={() => { setIsExpanded(false); onExpandChange?.(false); }}
|
| 80 |
+
> <div className="sidebar-brand">
|
| 81 |
<NavLink to="/dashboard" className="sidebar-logo">
|
| 82 |
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
| 83 |
<rect width="32" height="32" rx="8" fill="#005241"/>
|
|
|
|
| 89 |
</NavLink>
|
| 90 |
</div>
|
| 91 |
|
|
|
|
| 92 |
<nav className="sidebar-nav">
|
| 93 |
+
{allLinks.map((group) => (
|
| 94 |
<div key={group.section} className="sidebar-section">
|
| 95 |
<div className="sidebar-section-label">{group.section}</div>
|
| 96 |
{group.items.map((link) => (
|
|
|
|
| 107 |
))}
|
| 108 |
</nav>
|
| 109 |
|
|
|
|
| 110 |
<div className="sidebar-footer">
|
| 111 |
{user && (
|
| 112 |
<div className="sidebar-user">
|
|
|
|
| 128 |
</div>
|
| 129 |
</aside>
|
| 130 |
);
|
| 131 |
+
|
| 132 |
+
// ── Mobile Bottom Tab Bar + Drawer ───────────────────────────────
|
| 133 |
+
const mobileNav = (
|
| 134 |
+
<>
|
| 135 |
+
<nav className="mobile-nav">
|
| 136 |
+
<div className="mobile-nav-items">
|
| 137 |
+
{mobileNavItems.map((item) => (
|
| 138 |
+
<NavLink
|
| 139 |
+
key={item.to}
|
| 140 |
+
to={item.to}
|
| 141 |
+
className={({ isActive }) => `mobile-nav-item ${isActive ? 'active' : ''}`}
|
| 142 |
+
>
|
| 143 |
+
{item.icon}
|
| 144 |
+
<span>{item.label}</span>
|
| 145 |
+
</NavLink>
|
| 146 |
+
))}
|
| 147 |
+
{/* More button */}
|
| 148 |
+
<button
|
| 149 |
+
className={`mobile-nav-item ${drawerOpen ? 'active' : ''}`}
|
| 150 |
+
onClick={() => setDrawerOpen(!drawerOpen)}
|
| 151 |
+
>
|
| 152 |
+
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20">
|
| 153 |
+
<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"/>
|
| 154 |
+
</svg>
|
| 155 |
+
<span>More</span>
|
| 156 |
+
</button>
|
| 157 |
+
</div>
|
| 158 |
+
</nav>
|
| 159 |
+
|
| 160 |
+
{/* Slide-up drawer with all nav items */}
|
| 161 |
+
{drawerOpen && (
|
| 162 |
+
<>
|
| 163 |
+
<div className="mobile-drawer-overlay" onClick={() => setDrawerOpen(false)} />
|
| 164 |
+
<div className="mobile-drawer">
|
| 165 |
+
<div className="mobile-drawer-handle" />
|
| 166 |
+
{allLinks.map((group) => (
|
| 167 |
+
<div key={group.section} className="sidebar-section">
|
| 168 |
+
<div className="sidebar-section-label">{group.section}</div>
|
| 169 |
+
{group.items.map((link) => (
|
| 170 |
+
<NavLink
|
| 171 |
+
key={link.to}
|
| 172 |
+
to={link.to}
|
| 173 |
+
className={({ isActive }) => `sidebar-link ${isActive ? 'active' : ''}`}
|
| 174 |
+
onClick={() => setDrawerOpen(false)}
|
| 175 |
+
>
|
| 176 |
+
<span className="sidebar-icon">{link.icon}</span>
|
| 177 |
+
<span className="sidebar-label">{link.label}</span>
|
| 178 |
+
</NavLink>
|
| 179 |
+
))}
|
| 180 |
+
</div>
|
| 181 |
+
))}
|
| 182 |
+
{/* User + Logout in drawer */}
|
| 183 |
+
<div style={{ borderTop: '1px solid var(--border-subtle)', marginTop: '0.5rem', paddingTop: '0.75rem' }}>
|
| 184 |
+
{user && (
|
| 185 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.5rem 1rem', marginBottom: '0.5rem' }}>
|
| 186 |
+
<div className="sidebar-avatar">
|
| 187 |
+
{(user.username || 'U').charAt(0).toUpperCase()}
|
| 188 |
+
</div>
|
| 189 |
+
<div>
|
| 190 |
+
<div style={{ fontSize: '0.85rem', fontWeight: 600 }}>{user.full_name || user.username}</div>
|
| 191 |
+
<div style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>{user.email}</div>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
)}
|
| 195 |
+
<button className="sidebar-link sidebar-logout" onClick={handleLogout} style={{ width: '100%' }}>
|
| 196 |
+
<span className="sidebar-icon">
|
| 197 |
+
<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>
|
| 198 |
+
</span>
|
| 199 |
+
<span className="sidebar-label">Log Out</span>
|
| 200 |
+
</button>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
</>
|
| 204 |
+
)}
|
| 205 |
+
</>
|
| 206 |
+
);
|
| 207 |
+
|
| 208 |
+
return (
|
| 209 |
+
<>
|
| 210 |
+
{isMobile ? mobileNav : desktopSidebar}
|
| 211 |
+
</>
|
| 212 |
+
);
|
| 213 |
}
|
frontend/src/index.css
CHANGED
|
@@ -138,9 +138,8 @@ h3 { font-size: 1rem; font-family: var(--font-sans); font-weight: 600; }
|
|
| 138 |
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 139 |
z-index: 200;
|
| 140 |
}
|
| 141 |
-
.sidebar
|
| 142 |
width: 260px;
|
| 143 |
-
box-shadow: var(--shadow-lg);
|
| 144 |
}
|
| 145 |
|
| 146 |
/* Brand */
|
|
@@ -172,7 +171,7 @@ h3 { font-size: 1rem; font-family: var(--font-sans); font-weight: 600; }
|
|
| 172 |
transform: translateX(-8px);
|
| 173 |
transition: opacity 0.2s ease 0.05s, transform 0.2s ease 0.05s;
|
| 174 |
}
|
| 175 |
-
.sidebar
|
| 176 |
opacity: 1;
|
| 177 |
transform: translateX(0);
|
| 178 |
}
|
|
@@ -204,7 +203,7 @@ h3 { font-size: 1rem; font-family: var(--font-sans); font-weight: 600; }
|
|
| 204 |
opacity: 0;
|
| 205 |
transition: opacity 0.2s ease 0.05s;
|
| 206 |
}
|
| 207 |
-
.sidebar
|
| 208 |
opacity: 1;
|
| 209 |
}
|
| 210 |
|
|
@@ -256,7 +255,7 @@ h3 { font-size: 1rem; font-family: var(--font-sans); font-weight: 600; }
|
|
| 256 |
transform: translateX(-8px);
|
| 257 |
transition: opacity 0.2s ease 0.05s, transform 0.2s ease 0.05s;
|
| 258 |
}
|
| 259 |
-
.sidebar
|
| 260 |
opacity: 1;
|
| 261 |
transform: translateX(0);
|
| 262 |
}
|
|
@@ -293,7 +292,7 @@ h3 { font-size: 1rem; font-family: var(--font-sans); font-weight: 600; }
|
|
| 293 |
opacity: 0;
|
| 294 |
transition: opacity 0.2s ease 0.05s;
|
| 295 |
}
|
| 296 |
-
.sidebar
|
| 297 |
opacity: 1;
|
| 298 |
}
|
| 299 |
.sidebar-user-name {
|
|
@@ -566,12 +565,393 @@ tr:hover td { background: var(--bg-hover); }
|
|
| 566 |
}
|
| 567 |
.icon-box svg { width: 22px; height: 22px; }
|
| 568 |
|
| 569 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
@media (max-width: 768px) {
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
.
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
}
|
| 576 |
|
| 577 |
/* ── Scrollbar ───────────────────────────────────────────────────────── */
|
|
|
|
| 138 |
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 139 |
z-index: 200;
|
| 140 |
}
|
| 141 |
+
.sidebar.sidebar-expanded {
|
| 142 |
width: 260px;
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
/* Brand */
|
|
|
|
| 171 |
transform: translateX(-8px);
|
| 172 |
transition: opacity 0.2s ease 0.05s, transform 0.2s ease 0.05s;
|
| 173 |
}
|
| 174 |
+
.sidebar.sidebar-expanded .sidebar-brand-text {
|
| 175 |
opacity: 1;
|
| 176 |
transform: translateX(0);
|
| 177 |
}
|
|
|
|
| 203 |
opacity: 0;
|
| 204 |
transition: opacity 0.2s ease 0.05s;
|
| 205 |
}
|
| 206 |
+
.sidebar.sidebar-expanded .sidebar-section-label {
|
| 207 |
opacity: 1;
|
| 208 |
}
|
| 209 |
|
|
|
|
| 255 |
transform: translateX(-8px);
|
| 256 |
transition: opacity 0.2s ease 0.05s, transform 0.2s ease 0.05s;
|
| 257 |
}
|
| 258 |
+
.sidebar.sidebar-expanded .sidebar-label {
|
| 259 |
opacity: 1;
|
| 260 |
transform: translateX(0);
|
| 261 |
}
|
|
|
|
| 292 |
opacity: 0;
|
| 293 |
transition: opacity 0.2s ease 0.05s;
|
| 294 |
}
|
| 295 |
+
.sidebar.sidebar-expanded .sidebar-user-info {
|
| 296 |
opacity: 1;
|
| 297 |
}
|
| 298 |
.sidebar-user-name {
|
|
|
|
| 565 |
}
|
| 566 |
.icon-box svg { width: 22px; height: 22px; }
|
| 567 |
|
| 568 |
+
/* ═══════════════════════════════════════════════════════════════════════
|
| 569 |
+
MOBILE RESPONSIVE — max-width: 768px
|
| 570 |
+
All rules below ONLY apply on mobile. Desktop is untouched.
|
| 571 |
+
═══════════════════════════════════════════════════════════════════════ */
|
| 572 |
+
|
| 573 |
@media (max-width: 768px) {
|
| 574 |
+
|
| 575 |
+
/* ── App Shell ─────────────────────────────────────────────────────── */
|
| 576 |
+
.app-main {
|
| 577 |
+
margin-left: 0 !important;
|
| 578 |
+
padding-bottom: 72px; /* space for bottom tab bar */
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
/* ── Desktop Sidebar hidden on mobile ──────────────────────────────── */
|
| 582 |
+
.sidebar:not(.mobile-nav) {
|
| 583 |
+
display: none !important;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
/* ── Mobile Bottom Tab Bar ─────────────────────────────────────────── */
|
| 587 |
+
.mobile-nav {
|
| 588 |
+
display: flex !important;
|
| 589 |
+
position: fixed;
|
| 590 |
+
bottom: 0;
|
| 591 |
+
left: 0;
|
| 592 |
+
right: 0;
|
| 593 |
+
height: 64px;
|
| 594 |
+
background: var(--bg-primary);
|
| 595 |
+
border-top: 1px solid var(--border-color);
|
| 596 |
+
z-index: 300;
|
| 597 |
+
padding: 0 0.25rem;
|
| 598 |
+
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
|
| 599 |
+
}
|
| 600 |
+
.mobile-nav-items {
|
| 601 |
+
display: flex;
|
| 602 |
+
justify-content: space-around;
|
| 603 |
+
align-items: center;
|
| 604 |
+
width: 100%;
|
| 605 |
+
height: 100%;
|
| 606 |
+
}
|
| 607 |
+
.mobile-nav-item {
|
| 608 |
+
display: flex;
|
| 609 |
+
flex-direction: column;
|
| 610 |
+
align-items: center;
|
| 611 |
+
justify-content: center;
|
| 612 |
+
gap: 0.2rem;
|
| 613 |
+
padding: 0.35rem 0.5rem;
|
| 614 |
+
border-radius: var(--radius-md);
|
| 615 |
+
color: var(--text-muted);
|
| 616 |
+
text-decoration: none;
|
| 617 |
+
font-size: 0.6rem;
|
| 618 |
+
font-weight: 600;
|
| 619 |
+
letter-spacing: 0.02em;
|
| 620 |
+
transition: color var(--transition-fast);
|
| 621 |
+
background: none;
|
| 622 |
+
border: none;
|
| 623 |
+
cursor: pointer;
|
| 624 |
+
font-family: var(--font-sans);
|
| 625 |
+
}
|
| 626 |
+
.mobile-nav-item svg {
|
| 627 |
+
width: 20px;
|
| 628 |
+
height: 20px;
|
| 629 |
+
}
|
| 630 |
+
.mobile-nav-item.active,
|
| 631 |
+
.mobile-nav-item:hover {
|
| 632 |
+
color: var(--accent);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
/* ── Mobile drawer overlay ─────────────────────────────────────────── */
|
| 636 |
+
.mobile-drawer-overlay {
|
| 637 |
+
position: fixed;
|
| 638 |
+
inset: 0;
|
| 639 |
+
background: rgba(0, 0, 0, 0.4);
|
| 640 |
+
z-index: 400;
|
| 641 |
+
animation: fadeIn 0.2s ease;
|
| 642 |
+
}
|
| 643 |
+
.mobile-drawer {
|
| 644 |
+
position: fixed;
|
| 645 |
+
bottom: 0;
|
| 646 |
+
left: 0;
|
| 647 |
+
right: 0;
|
| 648 |
+
background: var(--bg-primary);
|
| 649 |
+
border-radius: 20px 20px 0 0;
|
| 650 |
+
padding: 1rem 0.75rem 2rem;
|
| 651 |
+
z-index: 500;
|
| 652 |
+
max-height: 70vh;
|
| 653 |
+
overflow-y: auto;
|
| 654 |
+
animation: slideUp 0.25s ease;
|
| 655 |
+
}
|
| 656 |
+
@keyframes slideUp {
|
| 657 |
+
from { transform: translateY(100%); }
|
| 658 |
+
to { transform: translateY(0); }
|
| 659 |
+
}
|
| 660 |
+
.mobile-drawer-handle {
|
| 661 |
+
width: 36px;
|
| 662 |
+
height: 4px;
|
| 663 |
+
background: var(--border-color);
|
| 664 |
+
border-radius: 2px;
|
| 665 |
+
margin: 0 auto 1rem;
|
| 666 |
+
}
|
| 667 |
+
.mobile-drawer .sidebar-link {
|
| 668 |
+
width: 100%;
|
| 669 |
+
padding: 0.75rem 1rem;
|
| 670 |
+
margin: 2px 0;
|
| 671 |
+
font-size: 0.88rem;
|
| 672 |
+
border-radius: var(--radius-md);
|
| 673 |
+
opacity: 1;
|
| 674 |
+
}
|
| 675 |
+
.mobile-drawer .sidebar-link .sidebar-label {
|
| 676 |
+
opacity: 1;
|
| 677 |
+
transform: none;
|
| 678 |
+
}
|
| 679 |
+
.mobile-drawer .sidebar-link .sidebar-icon {
|
| 680 |
+
width: 22px;
|
| 681 |
+
height: 22px;
|
| 682 |
+
}
|
| 683 |
+
.mobile-drawer .sidebar-section-label {
|
| 684 |
+
opacity: 1;
|
| 685 |
+
padding: 0.5rem 1rem 0.25rem;
|
| 686 |
+
font-size: 0.65rem;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
/* ── Page layout ───────────────────────────────────────────────────── */
|
| 690 |
+
.page {
|
| 691 |
+
padding: 1.25rem 1rem !important;
|
| 692 |
+
max-width: 100%;
|
| 693 |
+
}
|
| 694 |
+
.page-header {
|
| 695 |
+
margin-bottom: 1.25rem;
|
| 696 |
+
padding-bottom: 1rem;
|
| 697 |
+
}
|
| 698 |
+
.page-header h1 {
|
| 699 |
+
font-size: 1.5rem !important;
|
| 700 |
+
}
|
| 701 |
+
.page-header p {
|
| 702 |
+
font-size: 0.85rem;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
/* ── Grids collapse ────────────────────────────────────────────────── */
|
| 706 |
+
.grid-2,
|
| 707 |
+
.grid-3,
|
| 708 |
+
.grid-4 {
|
| 709 |
+
grid-template-columns: 1fr !important;
|
| 710 |
+
gap: 1rem !important;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
/* ── Cards ─────────────────────────────────────────────────────────── */
|
| 714 |
+
.card {
|
| 715 |
+
padding: 1.125rem !important;
|
| 716 |
+
border-radius: var(--radius-md);
|
| 717 |
+
}
|
| 718 |
+
.card-header {
|
| 719 |
+
margin-bottom: 1rem;
|
| 720 |
+
padding-bottom: 0.75rem;
|
| 721 |
+
flex-wrap: wrap;
|
| 722 |
+
gap: 0.5rem;
|
| 723 |
+
}
|
| 724 |
+
.card-header h3 {
|
| 725 |
+
font-size: 0.88rem;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
/* ── Tables ────────────────────────────────────────────────────────── */
|
| 729 |
+
.table-container {
|
| 730 |
+
margin: 0 -1.125rem;
|
| 731 |
+
border-radius: 0;
|
| 732 |
+
border-left: none;
|
| 733 |
+
border-right: none;
|
| 734 |
+
-webkit-overflow-scrolling: touch;
|
| 735 |
+
}
|
| 736 |
+
th, td {
|
| 737 |
+
padding: 0.5rem 0.625rem;
|
| 738 |
+
font-size: 0.72rem;
|
| 739 |
+
white-space: nowrap;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
/* ── Charts ────────────────────────────────────────────────────────── */
|
| 743 |
+
.chart-container {
|
| 744 |
+
height: 240px !important;
|
| 745 |
+
padding: 0.375rem;
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
/* ── Metrics ───────────────────────────────────────────────────────── */
|
| 749 |
+
.metric { padding: 0.875rem 0.75rem; }
|
| 750 |
+
.metric-value { font-size: 1.375rem; }
|
| 751 |
+
.metric-label { font-size: 0.6rem; }
|
| 752 |
+
|
| 753 |
+
/* ── Tabs ───────────────────────────────────────────────────────────── */
|
| 754 |
+
.tabs {
|
| 755 |
+
overflow-x: auto;
|
| 756 |
+
-webkit-overflow-scrolling: touch;
|
| 757 |
+
flex-wrap: nowrap;
|
| 758 |
+
gap: 0;
|
| 759 |
+
margin-bottom: 1.25rem;
|
| 760 |
+
scrollbar-width: none;
|
| 761 |
+
}
|
| 762 |
+
.tabs::-webkit-scrollbar { display: none; }
|
| 763 |
+
.tab {
|
| 764 |
+
padding: 0.625rem 0.875rem;
|
| 765 |
+
font-size: 0.7rem;
|
| 766 |
+
flex-shrink: 0;
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
/* ── Buttons ───────────────────────────────────────────────────────── */
|
| 770 |
+
.btn-lg {
|
| 771 |
+
padding: 0.75rem 1.5rem;
|
| 772 |
+
font-size: 0.8rem;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
/* ── Forms ─────────────────────────────────────────────────────────── */
|
| 776 |
+
.input, select, textarea {
|
| 777 |
+
font-size: 16px; /* prevents iOS zoom on focus */
|
| 778 |
+
}
|
| 779 |
+
.form-group label {
|
| 780 |
+
font-size: 0.7rem;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* ── Flex Utils ────────────────────────────────────────────────────── */
|
| 784 |
+
.flex-between {
|
| 785 |
+
flex-wrap: wrap;
|
| 786 |
+
gap: 0.5rem;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
/* ── Empty state ───────────────────────────────────────────────────── */
|
| 790 |
+
.empty-state {
|
| 791 |
+
padding: 2.5rem 1.5rem;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
/* ── Loading ───────────────────────────────────────────────────────── */
|
| 795 |
+
.loading-overlay {
|
| 796 |
+
padding: 3rem 1.5rem;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
/* ── Badge ─────────────────────────────────────────────────────────── */
|
| 800 |
+
.badge {
|
| 801 |
+
font-size: 0.6rem;
|
| 802 |
+
padding: 0.15rem 0.5rem;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
/* ── Icon box ──────────────────────────────────────────────────────── */
|
| 806 |
+
.icon-box {
|
| 807 |
+
width: 40px;
|
| 808 |
+
height: 40px;
|
| 809 |
+
}
|
| 810 |
+
.icon-box svg { width: 18px; height: 18px; }
|
| 811 |
+
|
| 812 |
+
/* ═════════════════════════════════════════════════════════════════════
|
| 813 |
+
PAGE-SPECIFIC MOBILE OVERRIDES
|
| 814 |
+
═════════════════════════════════════════════════════════════════════ */
|
| 815 |
+
|
| 816 |
+
/* ── Landing Page ──────────────────────────────────────────────────── */
|
| 817 |
+
.landing-header {
|
| 818 |
+
padding: 0.875rem 1rem !important;
|
| 819 |
+
}
|
| 820 |
+
.landing-header .brand-text {
|
| 821 |
+
font-size: 1.1rem;
|
| 822 |
+
}
|
| 823 |
+
.landing-hero {
|
| 824 |
+
min-height: 80vh !important;
|
| 825 |
+
padding: 1rem !important;
|
| 826 |
+
}
|
| 827 |
+
.landing-hero h1 {
|
| 828 |
+
font-size: clamp(1.75rem, 7vw, 2.5rem) !important;
|
| 829 |
+
line-height: 1.2 !important;
|
| 830 |
+
}
|
| 831 |
+
.landing-hero p {
|
| 832 |
+
font-size: 0.9rem !important;
|
| 833 |
+
}
|
| 834 |
+
.landing-hero .flex-gap {
|
| 835 |
+
flex-direction: column;
|
| 836 |
+
width: 100%;
|
| 837 |
+
}
|
| 838 |
+
.landing-hero .flex-gap .btn {
|
| 839 |
+
width: 100%;
|
| 840 |
+
}
|
| 841 |
+
.landing-section {
|
| 842 |
+
padding: 3rem 1.25rem !important;
|
| 843 |
+
}
|
| 844 |
+
.landing-footer {
|
| 845 |
+
flex-direction: column !important;
|
| 846 |
+
gap: 0.5rem;
|
| 847 |
+
text-align: center;
|
| 848 |
+
padding: 1.5rem 1rem !important;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
/* ── Login / Register ──────────────────────────────────────────────── */
|
| 852 |
+
.auth-container {
|
| 853 |
+
flex-direction: column !important;
|
| 854 |
+
}
|
| 855 |
+
.auth-card {
|
| 856 |
+
padding: 2rem 1.25rem !important;
|
| 857 |
+
min-height: 100vh;
|
| 858 |
+
}
|
| 859 |
+
.auth-image {
|
| 860 |
+
display: none !important;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
/* ── Dashboard ─────────────────────────────────────────────────────── */
|
| 864 |
+
.dashboard-ticker-strip {
|
| 865 |
+
gap: 0.375rem !important;
|
| 866 |
+
}
|
| 867 |
+
.dashboard-ticker-strip button {
|
| 868 |
+
min-width: 120px !important;
|
| 869 |
+
padding: 0.5rem 0.625rem !important;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
/* ── Market Explorer ───────────────────────────────────────────────── */
|
| 873 |
+
.market-search-bar {
|
| 874 |
+
flex-direction: column !important;
|
| 875 |
+
gap: 0.75rem !important;
|
| 876 |
+
}
|
| 877 |
+
.market-search-bar .input {
|
| 878 |
+
width: 100% !important;
|
| 879 |
+
}
|
| 880 |
+
.market-search-bar select {
|
| 881 |
+
width: 100% !important;
|
| 882 |
+
}
|
| 883 |
+
.market-tabs {
|
| 884 |
+
overflow-x: auto !important;
|
| 885 |
+
flex-wrap: nowrap !important;
|
| 886 |
+
}
|
| 887 |
+
.ticker-chips {
|
| 888 |
+
flex-wrap: wrap !important;
|
| 889 |
+
gap: 0.375rem !important;
|
| 890 |
+
}
|
| 891 |
+
.company-info-grid {
|
| 892 |
+
grid-template-columns: 1fr 1fr !important;
|
| 893 |
+
gap: 0.625rem !important;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
/* ── Factor Analysis ───────────────────────────────────────────────── */
|
| 897 |
+
.factor-input-bar {
|
| 898 |
+
flex-direction: column !important;
|
| 899 |
+
gap: 0.75rem !important;
|
| 900 |
+
}
|
| 901 |
+
.factor-input-bar .input {
|
| 902 |
+
width: 100% !important;
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
/* ── Sentiment ─────────────────────────────────────────────────────── */
|
| 906 |
+
.sentiment-gauge-row {
|
| 907 |
+
grid-template-columns: 1fr !important;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
/* ── Holdings ──────────────────────────────────────────────────────── */
|
| 911 |
+
.holdings-actions {
|
| 912 |
+
flex-direction: column !important;
|
| 913 |
+
gap: 0.5rem !important;
|
| 914 |
+
}
|
| 915 |
+
.holdings-actions .btn {
|
| 916 |
+
width: 100%;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
/* ── Strategy Builder ──────────────────────────────────────────────── */
|
| 920 |
+
.strategy-form-row {
|
| 921 |
+
flex-direction: column !important;
|
| 922 |
+
gap: 0.75rem !important;
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
/* ── Portfolio Analysis ────────────────────────────────────────────── */
|
| 926 |
+
.portfolio-stats-grid {
|
| 927 |
+
grid-template-columns: 1fr 1fr !important;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
/* ── Recharts responsive ───────────────────────────────────────────── */
|
| 931 |
+
.recharts-wrapper {
|
| 932 |
+
font-size: 0.7rem;
|
| 933 |
+
}
|
| 934 |
+
.recharts-cartesian-axis-tick-value {
|
| 935 |
+
font-size: 0.65rem;
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
/* ── Generic inline-style overrides using attribute selectors ───── */
|
| 939 |
+
/* Override inline grid/flex styles that use fixed column widths */
|
| 940 |
+
[style*="grid-template-columns: repeat"] {
|
| 941 |
+
grid-template-columns: 1fr !important;
|
| 942 |
+
}
|
| 943 |
+
[style*="gridTemplateColumns"] {
|
| 944 |
+
grid-template-columns: 1fr !important;
|
| 945 |
+
}
|
| 946 |
+
[style*="padding: 6rem 3rem"],
|
| 947 |
+
[style*="padding:'6rem 3rem'"] {
|
| 948 |
+
padding: 3rem 1.25rem !important;
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
/* Fix inline flex layouts that don't wrap */
|
| 952 |
+
[style*="display: flex"][style*="gap:"] {
|
| 953 |
+
flex-wrap: wrap;
|
| 954 |
+
}
|
| 955 |
}
|
| 956 |
|
| 957 |
/* ── Scrollbar ───────────────────────────────────────────────────────── */
|
frontend/src/pages/Dashboard.tsx
CHANGED
|
@@ -105,7 +105,7 @@ export default function Dashboard() {
|
|
| 105 |
</div>
|
| 106 |
|
| 107 |
{/* Global Market Ticker Strip */}
|
| 108 |
-
<div style={{display:'flex',gap:'0.5rem',marginBottom:'1.5rem',overflowX:'auto',paddingBottom:'0.25rem'}}>
|
| 109 |
{GLOBAL_INDICES.map(idx => {
|
| 110 |
const data = globalPrices[idx.ticker];
|
| 111 |
const change = data?.change || 0;
|
|
|
|
| 105 |
</div>
|
| 106 |
|
| 107 |
{/* Global Market Ticker Strip */}
|
| 108 |
+
<div className="dashboard-ticker-strip" style={{display:'flex',gap:'0.5rem',marginBottom:'1.5rem',overflowX:'auto',paddingBottom:'0.25rem'}}>
|
| 109 |
{GLOBAL_INDICES.map(idx => {
|
| 110 |
const data = globalPrices[idx.ticker];
|
| 111 |
const change = data?.change || 0;
|
frontend/src/pages/FactorAnalysis.tsx
CHANGED
|
@@ -78,7 +78,7 @@ export default function FactorAnalysis() {
|
|
| 78 |
|
| 79 |
{/* Input */}
|
| 80 |
<div className="card" style={{marginBottom:'1.5rem'}}>
|
| 81 |
-
<div style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}>
|
| 82 |
<div style={{flex:1,minWidth:300}}>
|
| 83 |
<label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',display:'block',marginBottom:'0.375rem'}}>Ticker Universe (comma-separated)</label>
|
| 84 |
<input className="input" value={tickers} onChange={e => setTickers(e.target.value)} placeholder="AAPL, MSFT, GOOGL..." />
|
|
|
|
| 78 |
|
| 79 |
{/* Input */}
|
| 80 |
<div className="card" style={{marginBottom:'1.5rem'}}>
|
| 81 |
+
<div className="factor-input-bar" style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}>
|
| 82 |
<div style={{flex:1,minWidth:300}}>
|
| 83 |
<label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',display:'block',marginBottom:'0.375rem'}}>Ticker Universe (comma-separated)</label>
|
| 84 |
<input className="input" value={tickers} onChange={e => setTickers(e.target.value)} placeholder="AAPL, MSFT, GOOGL..." />
|
frontend/src/pages/Landing.tsx
CHANGED
|
@@ -36,7 +36,7 @@ export default function Landing() {
|
|
| 36 |
return (
|
| 37 |
<div style={{minHeight:'100vh',background:'var(--bg-primary)'}}>
|
| 38 |
{/* ── Top Nav ────────────────────────────────────────────── */}
|
| 39 |
-
<header style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'1.25rem 3rem',position:'absolute',top:0,left:0,right:0,zIndex:10}}>
|
| 40 |
<div style={{fontFamily:'var(--font-serif)',fontSize:'1.3rem',fontWeight:600,color:'#fff',display:'flex',alignItems:'center',gap:'0.5rem'}}>
|
| 41 |
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
|
| 42 |
<rect width="32" height="32" rx="6" fill="rgba(255,255,255,0.15)"/>
|
|
@@ -51,7 +51,7 @@ export default function Landing() {
|
|
| 51 |
</header>
|
| 52 |
|
| 53 |
{/* ── Hero ──────────────────────────────────────────────── */}
|
| 54 |
-
<section style={{
|
| 55 |
position:'relative',
|
| 56 |
minHeight:'92vh',
|
| 57 |
display:'flex',
|
|
@@ -86,7 +86,7 @@ export default function Landing() {
|
|
| 86 |
</section>
|
| 87 |
|
| 88 |
{/* ── Capabilities ──────────────────────────────────────── */}
|
| 89 |
-
<section style={{padding:'6rem 3rem',maxWidth:'1200px',margin:'0 auto'}}>
|
| 90 |
<div style={{textAlign:'center',marginBottom:'4rem'}}>
|
| 91 |
<p style={{fontSize:'0.75rem',fontWeight:600,letterSpacing:'0.2em',textTransform:'uppercase',color:'var(--accent)',marginBottom:'0.75rem'}}>Capabilities</p>
|
| 92 |
<h2>What Powers Our Platform</h2>
|
|
@@ -106,7 +106,7 @@ export default function Landing() {
|
|
| 106 |
</section>
|
| 107 |
|
| 108 |
{/* ── Global Coverage ───────────────────────────────────── */}
|
| 109 |
-
<section style={{
|
| 110 |
padding:'6rem 3rem',
|
| 111 |
backgroundImage:'url(/images/pattern-bg.png)',
|
| 112 |
backgroundSize:'cover',
|
|
@@ -134,7 +134,7 @@ export default function Landing() {
|
|
| 134 |
</section>
|
| 135 |
|
| 136 |
{/* ── CTA ───────────────────────────────────────────────── */}
|
| 137 |
-
<section style={{
|
| 138 |
position:'relative',
|
| 139 |
padding:'6rem 3rem',
|
| 140 |
backgroundImage:'url(/images/buildings-bg.png)',
|
|
@@ -156,7 +156,7 @@ export default function Landing() {
|
|
| 156 |
</section>
|
| 157 |
|
| 158 |
{/* ── Footer ────────────────────────────────────────────── */}
|
| 159 |
-
<footer style={{padding:'2rem 3rem',borderTop:'1px solid var(--border-color)',display:'flex',justifyContent:'space-between',alignItems:'center'}}>
|
| 160 |
<p style={{color:'var(--text-muted)',fontSize:'0.75rem'}}>
|
| 161 |
© 2026 QuantHedge. All rights reserved. For research and educational purposes only.
|
| 162 |
</p>
|
|
|
|
| 36 |
return (
|
| 37 |
<div style={{minHeight:'100vh',background:'var(--bg-primary)'}}>
|
| 38 |
{/* ── Top Nav ────────────────────────────────────────────── */}
|
| 39 |
+
<header className="landing-header" style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'1.25rem 3rem',position:'absolute',top:0,left:0,right:0,zIndex:10}}>
|
| 40 |
<div style={{fontFamily:'var(--font-serif)',fontSize:'1.3rem',fontWeight:600,color:'#fff',display:'flex',alignItems:'center',gap:'0.5rem'}}>
|
| 41 |
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
|
| 42 |
<rect width="32" height="32" rx="6" fill="rgba(255,255,255,0.15)"/>
|
|
|
|
| 51 |
</header>
|
| 52 |
|
| 53 |
{/* ── Hero ──────────────────────────────────────────────── */}
|
| 54 |
+
<section className="landing-hero" style={{
|
| 55 |
position:'relative',
|
| 56 |
minHeight:'92vh',
|
| 57 |
display:'flex',
|
|
|
|
| 86 |
</section>
|
| 87 |
|
| 88 |
{/* ── Capabilities ──────────────────────────────────────── */}
|
| 89 |
+
<section className="landing-section" style={{padding:'6rem 3rem',maxWidth:'1200px',margin:'0 auto'}}>
|
| 90 |
<div style={{textAlign:'center',marginBottom:'4rem'}}>
|
| 91 |
<p style={{fontSize:'0.75rem',fontWeight:600,letterSpacing:'0.2em',textTransform:'uppercase',color:'var(--accent)',marginBottom:'0.75rem'}}>Capabilities</p>
|
| 92 |
<h2>What Powers Our Platform</h2>
|
|
|
|
| 106 |
</section>
|
| 107 |
|
| 108 |
{/* ── Global Coverage ───────────────────────────────────── */}
|
| 109 |
+
<section className="landing-section" style={{
|
| 110 |
padding:'6rem 3rem',
|
| 111 |
backgroundImage:'url(/images/pattern-bg.png)',
|
| 112 |
backgroundSize:'cover',
|
|
|
|
| 134 |
</section>
|
| 135 |
|
| 136 |
{/* ── CTA ───────────────────────────────────────────────── */}
|
| 137 |
+
<section className="landing-section" style={{
|
| 138 |
position:'relative',
|
| 139 |
padding:'6rem 3rem',
|
| 140 |
backgroundImage:'url(/images/buildings-bg.png)',
|
|
|
|
| 156 |
</section>
|
| 157 |
|
| 158 |
{/* ── Footer ────────────────────────────────────────────── */}
|
| 159 |
+
<footer className="landing-footer" style={{padding:'2rem 3rem',borderTop:'1px solid var(--border-color)',display:'flex',justifyContent:'space-between',alignItems:'center'}}>
|
| 160 |
<p style={{color:'var(--text-muted)',fontSize:'0.75rem'}}>
|
| 161 |
© 2026 QuantHedge. All rights reserved. For research and educational purposes only.
|
| 162 |
</p>
|
frontend/src/pages/Login.tsx
CHANGED
|
@@ -25,9 +25,9 @@ export default function Login() {
|
|
| 25 |
};
|
| 26 |
|
| 27 |
return (
|
| 28 |
-
<div style={{minHeight:'100vh',display:'flex'}}>
|
| 29 |
{/* Left — form */}
|
| 30 |
-
<div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',padding:'3rem'}}>
|
| 31 |
<div style={{width:'100%',maxWidth:'400px'}}>
|
| 32 |
<div style={{marginBottom:'2.5rem'}}>
|
| 33 |
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'2rem'}}>
|
|
@@ -70,7 +70,7 @@ export default function Login() {
|
|
| 70 |
</div>
|
| 71 |
|
| 72 |
{/* Right — image */}
|
| 73 |
-
<div style={{
|
| 74 |
flex:1,
|
| 75 |
backgroundImage:'url(/images/buildings-bg.png)',
|
| 76 |
backgroundSize:'cover',
|
|
|
|
| 25 |
};
|
| 26 |
|
| 27 |
return (
|
| 28 |
+
<div className="auth-container" style={{minHeight:'100vh',display:'flex'}}>
|
| 29 |
{/* Left — form */}
|
| 30 |
+
<div className="auth-card" style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',padding:'3rem'}}>
|
| 31 |
<div style={{width:'100%',maxWidth:'400px'}}>
|
| 32 |
<div style={{marginBottom:'2.5rem'}}>
|
| 33 |
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'2rem'}}>
|
|
|
|
| 70 |
</div>
|
| 71 |
|
| 72 |
{/* Right — image */}
|
| 73 |
+
<div className="auth-image" style={{
|
| 74 |
flex:1,
|
| 75 |
backgroundImage:'url(/images/buildings-bg.png)',
|
| 76 |
backgroundSize:'cover',
|
frontend/src/pages/MarketExplorer.tsx
CHANGED
|
@@ -50,7 +50,7 @@ export default function MarketExplorer() {
|
|
| 50 |
|
| 51 |
{/* Search */}
|
| 52 |
<div className="card" style={{marginBottom:'1.5rem'}}>
|
| 53 |
-
<div style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}>
|
| 54 |
<div style={{flex:1,minWidth:200}}>
|
| 55 |
<label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',display:'block',marginBottom:'0.375rem'}}>Ticker Symbol</label>
|
| 56 |
<TickerSearch value={ticker} onChange={v => setTicker(v.toUpperCase())} onSelect={t => setTicker(t.symbol)} placeholder="Search any ticker..." />
|
|
@@ -73,7 +73,7 @@ export default function MarketExplorer() {
|
|
| 73 |
<button key={m} className={`tab ${activeMarket === m ? 'active' : ''}`} onClick={() => loadPopularTickers(m)}>{m}</button>
|
| 74 |
))}
|
| 75 |
</div>
|
| 76 |
-
<div style={{display:'flex',gap:'0.5rem',flexWrap:'wrap'}}>
|
| 77 |
{popularTickers.slice(0, 15).map(t => (
|
| 78 |
<button key={t} className="btn btn-secondary btn-sm" style={{fontFamily:'var(--font-mono)',fontSize:'0.75rem'}}
|
| 79 |
onClick={() => search(t)}>{t}</button>
|
|
@@ -113,7 +113,7 @@ export default function MarketExplorer() {
|
|
| 113 |
{companyInfo && !companyInfo.error && (
|
| 114 |
<div className="card">
|
| 115 |
<div className="card-header"><h3>Company Info</h3></div>
|
| 116 |
-
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'0.75rem'}}>
|
| 117 |
{[
|
| 118 |
['Sector', companyInfo.sector],
|
| 119 |
['Industry', companyInfo.industry],
|
|
|
|
| 50 |
|
| 51 |
{/* Search */}
|
| 52 |
<div className="card" style={{marginBottom:'1.5rem'}}>
|
| 53 |
+
<div className="market-search-bar" style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}>
|
| 54 |
<div style={{flex:1,minWidth:200}}>
|
| 55 |
<label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',display:'block',marginBottom:'0.375rem'}}>Ticker Symbol</label>
|
| 56 |
<TickerSearch value={ticker} onChange={v => setTicker(v.toUpperCase())} onSelect={t => setTicker(t.symbol)} placeholder="Search any ticker..." />
|
|
|
|
| 73 |
<button key={m} className={`tab ${activeMarket === m ? 'active' : ''}`} onClick={() => loadPopularTickers(m)}>{m}</button>
|
| 74 |
))}
|
| 75 |
</div>
|
| 76 |
+
<div className="ticker-chips" style={{display:'flex',gap:'0.5rem',flexWrap:'wrap'}}>
|
| 77 |
{popularTickers.slice(0, 15).map(t => (
|
| 78 |
<button key={t} className="btn btn-secondary btn-sm" style={{fontFamily:'var(--font-mono)',fontSize:'0.75rem'}}
|
| 79 |
onClick={() => search(t)}>{t}</button>
|
|
|
|
| 113 |
{companyInfo && !companyInfo.error && (
|
| 114 |
<div className="card">
|
| 115 |
<div className="card-header"><h3>Company Info</h3></div>
|
| 116 |
+
<div className="company-info-grid" style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'0.75rem'}}>
|
| 117 |
{[
|
| 118 |
['Sector', companyInfo.sector],
|
| 119 |
['Industry', companyInfo.industry],
|
frontend/src/pages/PortfolioAnalysis.tsx
CHANGED
|
@@ -13,6 +13,7 @@ export default function PortfolioAnalysis() {
|
|
| 13 |
const [activeTab, setActiveTab] = useState('optimize');
|
| 14 |
const [corrData, setCorrData] = useState<any>(null);
|
| 15 |
const [corrLoading, setCorrLoading] = useState(false);
|
|
|
|
| 16 |
|
| 17 |
const optimize = async () => {
|
| 18 |
setLoading(true);
|
|
@@ -27,6 +28,25 @@ export default function PortfolioAnalysis() {
|
|
| 27 |
} catch (e) { console.error(e); } finally { setLoading(false); }
|
| 28 |
};
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
const pieData = weights?.weights ? Object.entries(weights.weights).filter(([,v]: any) => v > 0.001).map(([ticker, weight]: any) => ({
|
| 31 |
name: ticker, value: parseFloat((weight * 100).toFixed(1)),
|
| 32 |
})) : [];
|
|
@@ -42,7 +62,26 @@ export default function PortfolioAnalysis() {
|
|
| 42 |
<div className="card" style={{marginBottom:'1.5rem'}}>
|
| 43 |
<div style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}>
|
| 44 |
<div style={{flex:1,minWidth:300}}>
|
| 45 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
<input className="input" value={tickers} onChange={e => setTickers(e.target.value)} />
|
| 47 |
</div>
|
| 48 |
<div>
|
|
|
|
| 13 |
const [activeTab, setActiveTab] = useState('optimize');
|
| 14 |
const [corrData, setCorrData] = useState<any>(null);
|
| 15 |
const [corrLoading, setCorrLoading] = useState(false);
|
| 16 |
+
const [holdingsLoading, setHoldingsLoading] = useState(false);
|
| 17 |
|
| 18 |
const optimize = async () => {
|
| 19 |
setLoading(true);
|
|
|
|
| 28 |
} catch (e) { console.error(e); } finally { setLoading(false); }
|
| 29 |
};
|
| 30 |
|
| 31 |
+
const useMyHoldings = async () => {
|
| 32 |
+
setHoldingsLoading(true);
|
| 33 |
+
try {
|
| 34 |
+
const res = await holdingsAPI.summary();
|
| 35 |
+
const holdings = res.data.holdings || [];
|
| 36 |
+
if (holdings.length === 0) {
|
| 37 |
+
alert('No holdings found. Add positions in the Holdings section first.');
|
| 38 |
+
return;
|
| 39 |
+
}
|
| 40 |
+
const holdingTickers = holdings.map((h: any) => h.ticker).join(', ');
|
| 41 |
+
setTickers(holdingTickers);
|
| 42 |
+
} catch (e) {
|
| 43 |
+
console.error(e);
|
| 44 |
+
alert('Failed to fetch holdings. Please try again.');
|
| 45 |
+
} finally {
|
| 46 |
+
setHoldingsLoading(false);
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
const pieData = weights?.weights ? Object.entries(weights.weights).filter(([,v]: any) => v > 0.001).map(([ticker, weight]: any) => ({
|
| 51 |
name: ticker, value: parseFloat((weight * 100).toFixed(1)),
|
| 52 |
})) : [];
|
|
|
|
| 62 |
<div className="card" style={{marginBottom:'1.5rem'}}>
|
| 63 |
<div style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}>
|
| 64 |
<div style={{flex:1,minWidth:300}}>
|
| 65 |
+
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'0.375rem'}}>
|
| 66 |
+
<label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em'}}>Portfolio Assets</label>
|
| 67 |
+
<button
|
| 68 |
+
onClick={useMyHoldings}
|
| 69 |
+
disabled={holdingsLoading}
|
| 70 |
+
style={{
|
| 71 |
+
background:'none',border:'1px solid var(--border-color)',borderRadius:'var(--radius-sm)',
|
| 72 |
+
padding:'0.2rem 0.6rem',fontSize:'0.7rem',fontWeight:600,color:'var(--accent)',
|
| 73 |
+
cursor:'pointer',display:'flex',alignItems:'center',gap:'0.3rem',
|
| 74 |
+
transition:'all 150ms ease',fontFamily:'var(--font-sans)',
|
| 75 |
+
}}
|
| 76 |
+
onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent-lighter)'; e.currentTarget.style.borderColor = 'var(--accent)'; }}
|
| 77 |
+
onMouseLeave={e => { e.currentTarget.style.background = 'none'; e.currentTarget.style.borderColor = 'var(--border-color)'; }}
|
| 78 |
+
>
|
| 79 |
+
{holdingsLoading ? <div className="spinner" style={{width:10,height:10,borderWidth:1.5}} /> : (
|
| 80 |
+
<svg width="12" height="12" 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>
|
| 81 |
+
)}
|
| 82 |
+
Use My Holdings
|
| 83 |
+
</button>
|
| 84 |
+
</div>
|
| 85 |
<input className="input" value={tickers} onChange={e => setTickers(e.target.value)} />
|
| 86 |
</div>
|
| 87 |
<div>
|
frontend/src/pages/Register.tsx
CHANGED
|
@@ -25,9 +25,9 @@ export default function Register() {
|
|
| 25 |
};
|
| 26 |
|
| 27 |
return (
|
| 28 |
-
<div style={{minHeight:'100vh',display:'flex'}}>
|
| 29 |
{/* Left — image */}
|
| 30 |
-
<div style={{
|
| 31 |
flex:1,
|
| 32 |
backgroundImage:'url(/images/hero-bg.png)',
|
| 33 |
backgroundSize:'cover',
|
|
@@ -47,7 +47,7 @@ export default function Register() {
|
|
| 47 |
</div>
|
| 48 |
|
| 49 |
{/* Right — form */}
|
| 50 |
-
<div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',padding:'3rem'}}>
|
| 51 |
<div style={{width:'100%',maxWidth:'400px'}}>
|
| 52 |
<div style={{marginBottom:'2.5rem'}}>
|
| 53 |
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'2rem'}}>
|
|
|
|
| 25 |
};
|
| 26 |
|
| 27 |
return (
|
| 28 |
+
<div className="auth-container" style={{minHeight:'100vh',display:'flex'}}>
|
| 29 |
{/* Left — image */}
|
| 30 |
+
<div className="auth-image" style={{
|
| 31 |
flex:1,
|
| 32 |
backgroundImage:'url(/images/hero-bg.png)',
|
| 33 |
backgroundSize:'cover',
|
|
|
|
| 47 |
</div>
|
| 48 |
|
| 49 |
{/* Right — form */}
|
| 50 |
+
<div className="auth-card" style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',padding:'3rem'}}>
|
| 51 |
<div style={{width:'100%',maxWidth:'400px'}}>
|
| 52 |
<div style={{marginBottom:'2.5rem'}}>
|
| 53 |
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'2rem'}}>
|