KSS-1227 commited on
Commit
2de9430
Β·
1 Parent(s): bfe0820

Add WhatsApp multi-user broadcast system

Browse files
frontend/src/components/layout/AppLayout.jsx CHANGED
@@ -1,112 +1,97 @@
1
- import React, { useState, useEffect } from "react";
2
  import { Outlet, useLocation } from "react-router-dom";
3
- import { AnimatePresence } from "framer-motion";
4
  import { TopBar } from "./TopBar";
5
- import { Sidebar } from "./Sidebar";
6
  import { BottomNavigation } from "./BottomNavigation";
7
  import { useIsMobile } from "../../hooks/useIsMobile";
8
  import { useGoldPrice } from "../../hooks/useMarketData";
9
 
10
- /**
11
- * AppLayout
12
- *
13
- * The single shell that wraps every authenticated page.
14
- *
15
- * Desktop: Fixed sidebar (collapsible) + sticky top bar + scrollable main
16
- * Mobile: Sticky top bar + scrollable main + fixed bottom navigation
17
- *
18
- * Sidebar width is tracked in state and passed as a CSS variable so the
19
- * main area can shift without a layout jump.
20
- */
21
  export function AppLayout() {
22
  const isMobile = useIsMobile();
23
  const { gold } = useGoldPrice();
24
  const location = useLocation();
25
 
26
- // Sidebar width β€” 220 expanded, 64 collapsed.
27
- // We sync this from Sidebar via a CSS custom property so the main
28
- // margin can transition smoothly without prop drilling.
29
- const [sidebarWidth, setSidebarWidth] = useState(220);
 
30
 
31
- // Listen for sidebar collapse events via a CSS variable observer.
32
- // Sidebar manages its own state; we just track the rendered width.
33
- useEffect(() => {
34
- const root = document.documentElement;
35
- root.style.setProperty("--sidebar-width", `${sidebarWidth}px`);
36
- }, [sidebarWidth]);
 
 
 
 
37
 
38
- // Reset sidebar observer when isMobile changes
 
 
 
39
  useEffect(() => {
40
- if (isMobile) {
41
- document.documentElement.style.setProperty("--sidebar-width", "0px");
42
- } else {
43
- document.documentElement.style.setProperty("--sidebar-width", `${sidebarWidth}px`);
44
- }
45
- }, [isMobile, sidebarWidth]);
46
 
47
  return (
48
- <div className="app-shell" style={{ minHeight: "100vh", background: "var(--bg-primary)" }}>
49
- {/* ── Top bar β€” always visible ───────────────────────── */}
50
- <TopBar gold={gold} isMobile={isMobile} />
51
 
52
- {/* ── Sidebar β€” desktop only ─────────────────────────── */}
53
- {!isMobile && (
54
- <SidebarWithWidthCallback onWidthChange={setSidebarWidth} />
55
- )}
 
 
56
 
57
- {/* ── Main content area ──────────────────────────────── */}
 
 
 
 
 
 
 
 
 
58
  <main
59
- className="app-main"
60
  id="main-content"
61
  tabIndex={-1}
62
  role="main"
63
  style={{
64
  marginTop: 52,
65
- marginLeft: isMobile ? 0 : "var(--sidebar-width, 220px)",
66
  minHeight: "calc(100vh - 52px)",
67
  padding: isMobile ? "16px 14px 80px" : "28px 32px 48px",
68
- transition: "margin-left 0.22s cubic-bezier(0.16, 1, 0.3, 1)",
69
  boxSizing: "border-box",
70
- maxWidth: isMobile ? "100%" : "calc(1080px + var(--sidebar-width, 220px))",
71
  }}
72
  >
73
  <AnimatePresence mode="wait" initial={false}>
74
- <Outlet
75
  key={location.pathname}
76
- context={{ gold, isMobile }}
77
- />
 
 
 
 
 
78
  </AnimatePresence>
79
  </main>
80
 
81
- {/* ── Bottom navigation β€” mobile only ────────────────── */}
82
  {isMobile && <BottomNavigation />}
83
  </div>
84
  );
85
  }
86
-
87
- /**
88
- * SidebarWithWidthCallback
89
- *
90
- * Wraps Sidebar and measures its rendered width via ResizeObserver
91
- * so AppLayout can update the main margin without prop drilling into Sidebar.
92
- */
93
- function SidebarWithWidthCallback({ onWidthChange }) {
94
- const ref = React.useRef(null);
95
-
96
- React.useEffect(() => {
97
- const el = ref.current;
98
- if (!el) return;
99
-
100
- const ro = new ResizeObserver(([entry]) => {
101
- onWidthChange(Math.round(entry.contentRect.width));
102
- });
103
- ro.observe(el);
104
- return () => ro.disconnect();
105
- }, [onWidthChange]);
106
-
107
- return (
108
- <div ref={ref} style={{ position: "fixed", top: 52, left: 0, bottom: 0, zIndex: 90 }}>
109
- <Sidebar />
110
- </div>
111
- );
112
- }
 
1
+ import React, { useCallback, useEffect, useState } from "react";
2
  import { Outlet, useLocation } from "react-router-dom";
3
+ import { AnimatePresence, motion } from "framer-motion";
4
  import { TopBar } from "./TopBar";
5
+ import { Sidebar, SIDEBAR_COLLAPSED, SIDEBAR_EXPANDED } from "./Sidebar";
6
  import { BottomNavigation } from "./BottomNavigation";
7
  import { useIsMobile } from "../../hooks/useIsMobile";
8
  import { useGoldPrice } from "../../hooks/useMarketData";
9
 
10
+ const LS_KEY = "tm_sidebar_collapsed";
11
+ const MAIN_TRANSITION = "margin-left 0.3s cubic-bezier(0.4,0,0.2,1)";
12
+
 
 
 
 
 
 
 
 
13
  export function AppLayout() {
14
  const isMobile = useIsMobile();
15
  const { gold } = useGoldPrice();
16
  const location = useLocation();
17
 
18
+ // Collapsed state β€” persisted in localStorage
19
+ const [collapsed, setCollapsed] = useState(() => {
20
+ try { return localStorage.getItem(LS_KEY) === "true"; }
21
+ catch { return false; }
22
+ });
23
 
24
+ // Mobile drawer open state
25
+ const [mobileOpen, setMobileOpen] = useState(false);
26
+
27
+ const toggleCollapsed = useCallback(() => {
28
+ setCollapsed((c) => {
29
+ const next = !c;
30
+ try { localStorage.setItem(LS_KEY, String(next)); } catch {}
31
+ return next;
32
+ });
33
+ }, []);
34
 
35
+ // Close mobile drawer on route change
36
+ useEffect(() => { setMobileOpen(false); }, [location.pathname]);
37
+
38
+ // Sync CSS variable for any consumers that read it
39
  useEffect(() => {
40
+ const w = isMobile ? 0 : collapsed ? SIDEBAR_COLLAPSED : SIDEBAR_EXPANDED;
41
+ document.documentElement.style.setProperty("--sidebar-width", `${w}px`);
42
+ }, [isMobile, collapsed]);
43
+
44
+ const sidebarW = isMobile ? 0 : collapsed ? SIDEBAR_COLLAPSED : SIDEBAR_EXPANDED;
 
45
 
46
  return (
47
+ <div style={{ minHeight: "100vh", background: "var(--bg-primary)" }}>
 
 
48
 
49
+ {/* Top bar β€” full width, always on top */}
50
+ <TopBar
51
+ gold={gold}
52
+ isMobile={isMobile}
53
+ onMenuClick={() => setMobileOpen(true)}
54
+ />
55
 
56
+ {/* Sidebar */}
57
+ <Sidebar
58
+ collapsed={collapsed}
59
+ onToggle={toggleCollapsed}
60
+ isMobile={isMobile}
61
+ mobileOpen={mobileOpen}
62
+ onMobileClose={() => setMobileOpen(false)}
63
+ />
64
+
65
+ {/* Main content */}
66
  <main
 
67
  id="main-content"
68
  tabIndex={-1}
69
  role="main"
70
  style={{
71
  marginTop: 52,
72
+ marginLeft: sidebarW,
73
  minHeight: "calc(100vh - 52px)",
74
  padding: isMobile ? "16px 14px 80px" : "28px 32px 48px",
75
+ transition: isMobile ? "none" : MAIN_TRANSITION,
76
  boxSizing: "border-box",
77
+ willChange: "margin-left",
78
  }}
79
  >
80
  <AnimatePresence mode="wait" initial={false}>
81
+ <motion.div
82
  key={location.pathname}
83
+ initial={{ opacity: 0, y: 8 }}
84
+ animate={{ opacity: 1, y: 0 }}
85
+ exit={{ opacity: 0, y: -4 }}
86
+ transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
87
+ >
88
+ <Outlet context={{ gold, isMobile }} />
89
+ </motion.div>
90
  </AnimatePresence>
91
  </main>
92
 
93
+ {/* Bottom nav β€” mobile only */}
94
  {isMobile && <BottomNavigation />}
95
  </div>
96
  );
97
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/layout/Sidebar.jsx CHANGED
@@ -1,201 +1,336 @@
1
- import React, { useState } from "react";
2
  import { NavLink } from "react-router-dom";
3
- import { ChevronLeft, ChevronRight, Sparkles } from "lucide-react";
4
- import { motion, AnimatePresence } from "framer-motion";
5
  import { NAV_ITEMS } from "../../constants/navigation";
6
 
7
- const SIDEBAR_W_EXPANDED = 220;
8
- const SIDEBAR_W_COLLAPSED = 64;
9
 
10
- export function Sidebar() {
11
- const [collapsed, setCollapsed] = useState(false);
12
- const [hovered, setHovered] = useState(null);
13
 
14
- const width = collapsed ? SIDEBAR_W_COLLAPSED : SIDEBAR_W_EXPANDED;
15
 
 
16
  return (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  <motion.aside
18
- animate={{ width }}
19
- transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}
20
  aria-label="Primary navigation"
21
  style={{
22
- position: "fixed",
23
- left: 0,
24
- top: 52,
25
- bottom: 0,
26
- width,
27
  background: "var(--bg-surface)",
28
  borderRight: "1px solid var(--border)",
29
  display: "flex",
30
  flexDirection: "column",
31
- zIndex: 90,
32
  overflow: "hidden",
 
33
  }}
34
  >
35
- {/* ── Nav Items ── */}
36
- <nav style={{ flex: 1, paddingTop: 10, paddingBottom: 8 }} role="navigation">
37
- {NAV_ITEMS.map((item) => {
38
- const Icon = item.icon;
39
- const showTooltip = collapsed && hovered === item.id;
40
-
41
- return (
42
- <div key={item.id} style={{ position: "relative" }}>
43
- <NavLink
44
- to={item.path}
45
- onMouseEnter={() => setHovered(item.id)}
46
- onMouseLeave={() => setHovered(null)}
47
- aria-label={item.label}
48
- style={({ isActive }) => ({
49
- display: "flex",
50
- alignItems: "center",
51
- gap: 11,
52
- height: 42,
53
- padding: collapsed ? "0 20px" : "0 14px",
54
- margin: "2px 8px",
55
- borderRadius: "var(--radius-md)",
56
- background: isActive
57
- ? "linear-gradient(135deg, rgba(0,201,167,0.14) 0%, rgba(0,201,167,0.06) 100%)"
58
- : "transparent",
59
- color: isActive ? "var(--color-teal)" : "var(--text-muted)",
60
- textDecoration: "none",
61
- cursor: "pointer",
62
- transition: "background var(--transition-fast), color var(--transition-fast)",
63
- whiteSpace: "nowrap",
64
- overflow: "hidden",
65
- position: "relative",
66
- border: isActive
67
- ? "1px solid rgba(0,201,167,0.2)"
68
- : "1px solid transparent",
69
- })}
70
  >
71
- {({ isActive }) => (
72
- <>
73
- {/* Animated left accent bar */}
74
- {isActive && (
75
- <motion.div
76
- layoutId="sidebarActiveIndicator"
77
- style={{
78
- position: "absolute",
79
- left: -8,
80
- top: "50%",
81
- transform: "translateY(-50%)",
82
- width: 3,
83
- height: 22,
84
- borderRadius: "0 2px 2px 0",
85
- background: "var(--color-teal)",
86
- boxShadow: "0 0 8px rgba(0,201,167,0.6)",
87
- }}
88
- transition={{ type: "spring", stiffness: 500, damping: 35 }}
89
- />
90
- )}
91
-
92
- <Icon
93
- size={17}
94
- style={{
95
- flexShrink: 0,
96
- color: isActive ? "var(--color-teal)" : "var(--text-muted)",
97
- transition: "color 0.15s ease",
98
- }}
99
- />
100
-
101
- <AnimatePresence>
102
- {!collapsed && (
103
- <motion.span
104
- initial={{ opacity: 0, x: -6 }}
105
- animate={{ opacity: 1, x: 0 }}
106
- exit={{ opacity: 0, x: -6 }}
107
- transition={{ duration: 0.15 }}
108
- style={{
109
- fontSize: 13,
110
- fontWeight: isActive ? 700 : 500,
111
- letterSpacing: isActive ? "-0.01em" : 0,
112
- }}
113
- >
114
- {item.label}
115
- </motion.span>
116
- )}
117
- </AnimatePresence>
118
-
119
- {/* AI badge for flagship features */}
120
- {!collapsed && item.ai && (
121
- <motion.span
122
- initial={{ opacity: 0, scale: 0.8 }}
123
- animate={{ opacity: 1, scale: 1 }}
124
- exit={{ opacity: 0, scale: 0.8 }}
125
- style={{
126
- marginLeft: "auto",
127
- fontSize: 9,
128
- fontWeight: 700,
129
- color: "var(--color-teal)",
130
- background: "var(--color-teal-bg)",
131
- border: "1px solid var(--color-teal-border)",
132
- borderRadius: "var(--radius-pill)",
133
- padding: "1px 5px",
134
- letterSpacing: "0.4px",
135
- }}
136
- >
137
- AI
138
- </motion.span>
139
- )}
140
- </>
141
- )}
142
- </NavLink>
143
-
144
- {/* Collapsed tooltip */}
145
- <AnimatePresence>
146
- {showTooltip && (
147
- <motion.div
148
- initial={{ opacity: 0, x: -6, scale: 0.95 }}
149
- animate={{ opacity: 1, x: 0, scale: 1 }}
150
- exit={{ opacity: 0, x: -6, scale: 0.95 }}
151
- transition={{ duration: 0.12 }}
152
- role="tooltip"
153
- style={{
154
- position: "absolute",
155
- left: SIDEBAR_W_COLLAPSED + 8,
156
- top: "50%",
157
- transform: "translateY(-50%)",
158
- background: "var(--bg-elevated)",
159
- border: "1px solid var(--border-hover)",
160
- borderRadius: "var(--radius-md)",
161
- padding: "5px 12px",
162
- fontSize: 12,
163
- fontWeight: 600,
164
- color: "var(--text-primary)",
165
- whiteSpace: "nowrap",
166
- pointerEvents: "none",
167
- zIndex: 300,
168
- boxShadow: "var(--shadow-lg)",
169
- }}
170
- >
171
- {item.label}
172
- </motion.div>
173
- )}
174
- </AnimatePresence>
175
- </div>
176
- );
177
- })}
178
  </nav>
179
 
180
- {/* ── AI Copilot Watermark (only expanded) ── */}
181
- <AnimatePresence>
182
- {!collapsed && (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  <motion.div
184
  initial={{ opacity: 0 }}
185
  animate={{ opacity: 1 }}
186
- exit={{ opacity: 0 }}
 
187
  style={{
188
- margin: "0 12px 8px",
189
  padding: "8px 10px",
190
  borderRadius: "var(--radius-md)",
191
  background: "linear-gradient(135deg, rgba(0,201,167,0.06) 0%, rgba(59,130,246,0.04) 100%)",
192
  border: "1px solid var(--border)",
193
  display: "flex",
194
  alignItems: "center",
195
- gap: 6,
196
  }}
197
  >
198
- <Sparkles size={12} color="var(--color-teal)" />
 
 
 
 
 
 
199
  <div>
200
  <div style={{ fontSize: 10, fontWeight: 700, color: "var(--color-teal)", letterSpacing: "0.4px" }}>
201
  TRADEMIND AI
@@ -206,34 +341,79 @@ export function Sidebar() {
206
  </div>
207
  </motion.div>
208
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  </AnimatePresence>
 
 
210
 
211
- {/* ── Collapse Toggle ── */}
212
- <button
213
- onClick={() => setCollapsed((c) => !c)}
214
- aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
215
- className="sidebar-collapse-btn"
216
- style={{
217
- display: "flex",
218
- alignItems: "center",
219
- justifyContent: collapsed ? "center" : "flex-start",
220
- gap: 8,
221
- margin: "0 8px 12px",
222
- padding: collapsed ? "9px 20px" : "9px 12px",
223
- borderRadius: "var(--radius-md)",
224
- background: "transparent",
225
- border: "1px solid var(--border)",
226
- color: "var(--text-muted)",
227
- cursor: "pointer",
228
- fontSize: 12,
229
- fontWeight: 600,
230
- transition: "all var(--transition-fast)",
231
- fontFamily: "var(--font-sans)",
232
- }}
233
- >
234
- {collapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
235
- {!collapsed && <span>Collapse</span>}
236
- </button>
237
- </motion.aside>
238
  );
239
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useState } from "react";
2
  import { NavLink } from "react-router-dom";
3
+ import { Menu, Sparkles, X } from "lucide-react";
4
+ import { AnimatePresence, motion } from "framer-motion";
5
  import { NAV_ITEMS } from "../../constants/navigation";
6
 
7
+ export const SIDEBAR_EXPANDED = 260;
8
+ export const SIDEBAR_COLLAPSED = 72;
9
 
10
+ const TRANSITION = { duration: 0.3, ease: [0.4, 0, 0.2, 1] };
11
+ const LABEL_TRANSITION = { duration: 0.18, ease: [0.4, 0, 0.2, 1] };
 
12
 
13
+ // ── Tooltip ──────────────────────────────────────────────────────────────────
14
 
15
+ function Tooltip({ label, visible }) {
16
  return (
17
+ <AnimatePresence>
18
+ {visible && (
19
+ <motion.div
20
+ initial={{ opacity: 0, x: -6, scale: 0.95 }}
21
+ animate={{ opacity: 1, x: 0, scale: 1 }}
22
+ exit={{ opacity: 0, x: -6, scale: 0.95 }}
23
+ transition={{ duration: 0.12 }}
24
+ role="tooltip"
25
+ style={{
26
+ position: "absolute",
27
+ left: SIDEBAR_COLLAPSED + 6,
28
+ top: "50%",
29
+ transform: "translateY(-50%)",
30
+ background: "var(--bg-elevated)",
31
+ border: "1px solid var(--border-hover)",
32
+ borderRadius: "var(--radius-md)",
33
+ padding: "5px 12px",
34
+ fontSize: 12,
35
+ fontWeight: 600,
36
+ color: "var(--text-primary)",
37
+ whiteSpace: "nowrap",
38
+ pointerEvents: "none",
39
+ zIndex: 400,
40
+ boxShadow: "var(--shadow-lg)",
41
+ }}
42
+ >
43
+ {label}
44
+ </motion.div>
45
+ )}
46
+ </AnimatePresence>
47
+ );
48
+ }
49
+
50
+ // ── Nav Item ─────────────────────────────────────────────────────────────────
51
+
52
+ function NavItem({ item, collapsed, onClose }) {
53
+ const [hovered, setHovered] = useState(false);
54
+ const Icon = item.icon;
55
+
56
+ return (
57
+ <div
58
+ style={{ position: "relative" }}
59
+ onMouseEnter={() => setHovered(true)}
60
+ onMouseLeave={() => setHovered(false)}
61
+ >
62
+ <NavLink
63
+ to={item.path}
64
+ onClick={onClose}
65
+ aria-label={item.label}
66
+ style={({ isActive }) => ({
67
+ display: "flex",
68
+ alignItems: "center",
69
+ gap: 10,
70
+ height: 40,
71
+ padding: collapsed ? "0" : "0 12px",
72
+ justifyContent: collapsed ? "center" : "flex-start",
73
+ margin: "1px 8px",
74
+ borderRadius: "var(--radius-md)",
75
+ background: isActive
76
+ ? "linear-gradient(135deg, rgba(0,201,167,0.14) 0%, rgba(0,201,167,0.06) 100%)"
77
+ : hovered
78
+ ? "rgba(255,255,255,0.04)"
79
+ : "transparent",
80
+ color: isActive ? "var(--color-teal)" : hovered ? "var(--text-primary)" : "var(--text-muted)",
81
+ textDecoration: "none",
82
+ cursor: "pointer",
83
+ transition: "background 0.15s ease, color 0.15s ease",
84
+ whiteSpace: "nowrap",
85
+ overflow: "hidden",
86
+ position: "relative",
87
+ border: isActive ? "1px solid rgba(0,201,167,0.18)" : "1px solid transparent",
88
+ boxShadow: isActive ? "0 0 12px rgba(0,201,167,0.08)" : "none",
89
+ })}
90
+ >
91
+ {({ isActive }) => (
92
+ <>
93
+ {/* Left active bar */}
94
+ {isActive && (
95
+ <motion.div
96
+ layoutId="activeBar"
97
+ style={{
98
+ position: "absolute",
99
+ left: -8,
100
+ top: "50%",
101
+ transform: "translateY(-50%)",
102
+ width: 3,
103
+ height: 20,
104
+ borderRadius: "0 2px 2px 0",
105
+ background: "var(--color-teal)",
106
+ boxShadow: "0 0 8px rgba(0,201,167,0.7)",
107
+ }}
108
+ transition={{ type: "spring", stiffness: 500, damping: 35 }}
109
+ />
110
+ )}
111
+
112
+ <Icon
113
+ size={17}
114
+ style={{
115
+ flexShrink: 0,
116
+ color: isActive ? "var(--color-teal)" : hovered ? "var(--text-primary)" : "var(--text-muted)",
117
+ transition: "color 0.15s ease",
118
+ }}
119
+ />
120
+
121
+ {/* Label */}
122
+ <AnimatePresence initial={false}>
123
+ {!collapsed && (
124
+ <motion.span
125
+ key="label"
126
+ initial={{ opacity: 0, width: 0 }}
127
+ animate={{ opacity: 1, width: "auto" }}
128
+ exit={{ opacity: 0, width: 0 }}
129
+ transition={LABEL_TRANSITION}
130
+ style={{
131
+ fontSize: 13,
132
+ fontWeight: isActive ? 700 : 500,
133
+ overflow: "hidden",
134
+ letterSpacing: isActive ? "-0.01em" : 0,
135
+ }}
136
+ >
137
+ {item.label}
138
+ </motion.span>
139
+ )}
140
+ </AnimatePresence>
141
+
142
+ {/* AI badge */}
143
+ <AnimatePresence initial={false}>
144
+ {!collapsed && item.ai && (
145
+ <motion.span
146
+ key="badge"
147
+ initial={{ opacity: 0, scale: 0.7 }}
148
+ animate={{ opacity: 1, scale: 1 }}
149
+ exit={{ opacity: 0, scale: 0.7 }}
150
+ transition={LABEL_TRANSITION}
151
+ style={{
152
+ marginLeft: "auto",
153
+ fontSize: 9,
154
+ fontWeight: 700,
155
+ color: "var(--color-teal)",
156
+ background: "var(--color-teal-bg)",
157
+ border: "1px solid var(--color-teal-border)",
158
+ borderRadius: "var(--radius-pill)",
159
+ padding: "1px 5px",
160
+ letterSpacing: "0.4px",
161
+ flexShrink: 0,
162
+ }}
163
+ >
164
+ AI
165
+ </motion.span>
166
+ )}
167
+ </AnimatePresence>
168
+ </>
169
+ )}
170
+ </NavLink>
171
+
172
+ {/* Tooltip β€” collapsed only */}
173
+ {collapsed && <Tooltip label={item.label} visible={hovered} />}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ // ── Sidebar ───────────────────────────────────────────────────────────────────
179
+
180
+ export function Sidebar({ collapsed, onToggle, isMobile, mobileOpen, onMobileClose }) {
181
+ const [aiHovered, setAiHovered] = useState(false);
182
+
183
+ // Mobile: close on Escape
184
+ useEffect(() => {
185
+ if (!isMobile) return;
186
+ const handler = (e) => { if (e.key === "Escape") onMobileClose?.(); };
187
+ window.addEventListener("keydown", handler);
188
+ return () => window.removeEventListener("keydown", handler);
189
+ }, [isMobile, onMobileClose]);
190
+
191
+ const sidebarContent = (
192
  <motion.aside
193
+ animate={{ width: collapsed ? SIDEBAR_COLLAPSED : SIDEBAR_EXPANDED }}
194
+ transition={TRANSITION}
195
  aria-label="Primary navigation"
196
  style={{
197
+ height: "100%",
 
 
 
 
198
  background: "var(--bg-surface)",
199
  borderRight: "1px solid var(--border)",
200
  display: "flex",
201
  flexDirection: "column",
 
202
  overflow: "hidden",
203
+ willChange: "width",
204
  }}
205
  >
206
+ {/* ── Header ── */}
207
+ <div
208
+ style={{
209
+ height: 52,
210
+ display: "flex",
211
+ alignItems: "center",
212
+ justifyContent: collapsed ? "center" : "space-between",
213
+ padding: collapsed ? "0" : "0 14px 0 16px",
214
+ borderBottom: "1px solid var(--border)",
215
+ flexShrink: 0,
216
+ }}
217
+ >
218
+ <AnimatePresence initial={false}>
219
+ {!collapsed && (
220
+ <motion.div
221
+ key="brand"
222
+ initial={{ opacity: 0 }}
223
+ animate={{ opacity: 1 }}
224
+ exit={{ opacity: 0 }}
225
+ transition={{ duration: 0.18 }}
226
+ style={{ display: "flex", alignItems: "center", gap: 8, overflow: "hidden" }}
227
+ >
228
+ <div
229
+ style={{
230
+ width: 28, height: 28, borderRadius: 7, flexShrink: 0,
231
+ background: "linear-gradient(135deg, rgba(0,201,167,0.2) 0%, rgba(0,201,167,0.08) 100%)",
232
+ border: "1px solid var(--color-teal-border)",
233
+ display: "flex", alignItems: "center", justifyContent: "center",
234
+ color: "var(--color-teal)",
235
+ }}
236
+ className="ai-pulse"
 
 
 
 
237
  >
238
+ <Sparkles size={14} />
239
+ </div>
240
+ <div>
241
+ <div style={{ fontSize: 13, fontWeight: 800, color: "var(--text-primary)", lineHeight: 1.2 }}>
242
+ TradeMind
243
+ </div>
244
+ <div style={{ fontSize: 8, color: "var(--color-teal)", letterSpacing: 1.2, fontWeight: 700 }}>
245
+ AI FINTECH Β· NSE INDIA
246
+ </div>
247
+ </div>
248
+ </motion.div>
249
+ )}
250
+ </AnimatePresence>
251
+
252
+ {/* Toggle button */}
253
+ {isMobile ? (
254
+ <button
255
+ onClick={onMobileClose}
256
+ aria-label="Close navigation"
257
+ style={btnStyle}
258
+ >
259
+ <X size={15} />
260
+ </button>
261
+ ) : (
262
+ <button
263
+ onClick={onToggle}
264
+ aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
265
+ style={btnStyle}
266
+ >
267
+ <motion.div
268
+ animate={{ rotate: collapsed ? 180 : 0 }}
269
+ transition={TRANSITION}
270
+ style={{ display: "flex", alignItems: "center" }}
271
+ >
272
+ <Menu size={15} />
273
+ </motion.div>
274
+ </button>
275
+ )}
276
+ </div>
277
+
278
+ {/* ── Nav ── */}
279
+ <nav
280
+ role="navigation"
281
+ style={{ flex: 1, paddingTop: 8, paddingBottom: 8, overflowY: "auto", overflowX: "hidden" }}
282
+ >
283
+ {NAV_ITEMS.map((item) => (
284
+ <NavItem
285
+ key={item.id}
286
+ item={item}
287
+ collapsed={collapsed}
288
+ onClose={isMobile ? onMobileClose : undefined}
289
+ />
290
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  </nav>
292
 
293
+ {/* ── AI Status bottom ── */}
294
+ <div style={{ padding: "8px", flexShrink: 0 }}>
295
+ {collapsed ? (
296
+ /* Collapsed: glowing dot */
297
+ <div
298
+ onMouseEnter={() => setAiHovered(true)}
299
+ onMouseLeave={() => setAiHovered(false)}
300
+ style={{ position: "relative", display: "flex", justifyContent: "center", padding: "8px 0" }}
301
+ >
302
+ <div
303
+ style={{
304
+ width: 8, height: 8, borderRadius: "50%",
305
+ background: "var(--color-teal)",
306
+ boxShadow: "0 0 8px rgba(0,201,167,0.8), 0 0 16px rgba(0,201,167,0.4)",
307
+ }}
308
+ />
309
+ <Tooltip label="TradeMind AI Β· RF Β· LSTM Β· FinBERT" visible={aiHovered} />
310
+ </div>
311
+ ) : (
312
  <motion.div
313
  initial={{ opacity: 0 }}
314
  animate={{ opacity: 1 }}
315
+ exit={{ opacity: 0 }}
316
+ transition={{ duration: 0.2 }}
317
  style={{
 
318
  padding: "8px 10px",
319
  borderRadius: "var(--radius-md)",
320
  background: "linear-gradient(135deg, rgba(0,201,167,0.06) 0%, rgba(59,130,246,0.04) 100%)",
321
  border: "1px solid var(--border)",
322
  display: "flex",
323
  alignItems: "center",
324
+ gap: 7,
325
  }}
326
  >
327
+ <div
328
+ style={{
329
+ width: 7, height: 7, borderRadius: "50%", flexShrink: 0,
330
+ background: "var(--color-teal)",
331
+ boxShadow: "0 0 6px rgba(0,201,167,0.8)",
332
+ }}
333
+ />
334
  <div>
335
  <div style={{ fontSize: 10, fontWeight: 700, color: "var(--color-teal)", letterSpacing: "0.4px" }}>
336
  TRADEMIND AI
 
341
  </div>
342
  </motion.div>
343
  )}
344
+ </div>
345
+ </motion.aside>
346
+ );
347
+
348
+ // ── Mobile: overlay drawer ──
349
+ if (isMobile) {
350
+ return (
351
+ <AnimatePresence>
352
+ {mobileOpen && (
353
+ <>
354
+ {/* Backdrop */}
355
+ <motion.div
356
+ key="backdrop"
357
+ initial={{ opacity: 0 }}
358
+ animate={{ opacity: 1 }}
359
+ exit={{ opacity: 0 }}
360
+ transition={{ duration: 0.2 }}
361
+ onClick={onMobileClose}
362
+ style={{
363
+ position: "fixed", inset: 0, zIndex: 89,
364
+ background: "rgba(0,0,0,0.55)",
365
+ backdropFilter: "blur(4px)",
366
+ WebkitBackdropFilter: "blur(4px)",
367
+ }}
368
+ />
369
+ {/* Drawer */}
370
+ <motion.div
371
+ key="drawer"
372
+ initial={{ x: -SIDEBAR_EXPANDED }}
373
+ animate={{ x: 0 }}
374
+ exit={{ x: -SIDEBAR_EXPANDED }}
375
+ transition={TRANSITION}
376
+ style={{
377
+ position: "fixed", top: 0, left: 0, bottom: 0,
378
+ width: SIDEBAR_EXPANDED, zIndex: 90,
379
+ }}
380
+ >
381
+ {sidebarContent}
382
+ </motion.div>
383
+ </>
384
+ )}
385
  </AnimatePresence>
386
+ );
387
+ }
388
 
389
+ // ── Desktop: fixed rail ──
390
+ return (
391
+ <div
392
+ style={{
393
+ position: "fixed", top: 52, left: 0, bottom: 0,
394
+ zIndex: 90,
395
+ width: collapsed ? SIDEBAR_COLLAPSED : SIDEBAR_EXPANDED,
396
+ transition: `width ${TRANSITION.duration}s cubic-bezier(0.4,0,0.2,1)`,
397
+ willChange: "width",
398
+ }}
399
+ >
400
+ {sidebarContent}
401
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  );
403
  }
404
+
405
+ // ── Shared button style ───────────────────────────────────────────────────────
406
+
407
+ const btnStyle = {
408
+ background: "transparent",
409
+ border: "1px solid var(--border)",
410
+ borderRadius: "var(--radius-md)",
411
+ padding: "6px 7px",
412
+ color: "var(--text-muted)",
413
+ cursor: "pointer",
414
+ display: "flex",
415
+ alignItems: "center",
416
+ justifyContent: "center",
417
+ flexShrink: 0,
418
+ transition: "border-color 0.15s ease, color 0.15s ease",
419
+ };
frontend/src/components/layout/TopBar.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useState, useEffect } from "react";
2
- import { TrendingUp, Bell, Search, LogOut, ChevronRight } from "lucide-react";
3
  import { useLocation } from "react-router-dom";
4
  import { motion, useScroll, useTransform } from "framer-motion";
5
  import { useAuth } from "../../context/AuthContext";
@@ -90,7 +90,7 @@ function GlobalSearch() {
90
  );
91
  }
92
 
93
- export function TopBar({ gold, isMobile }) {
94
  const { user, signOut } = useAuth();
95
  const location = useLocation();
96
  const time = useCurrentTime();
@@ -122,7 +122,7 @@ export function TopBar({ gold, isMobile }) {
122
  }}
123
  role="banner"
124
  >
125
- {/* Brand */}
126
  <div
127
  style={{
128
  display: "flex",
@@ -135,6 +135,25 @@ export function TopBar({ gold, isMobile }) {
135
  minWidth: isMobile ? 140 : 200,
136
  }}
137
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  <div
139
  style={{
140
  width: 30,
 
1
  import React, { useState, useEffect } from "react";
2
+ import { TrendingUp, Bell, Search, LogOut, ChevronRight, Menu } from "lucide-react";
3
  import { useLocation } from "react-router-dom";
4
  import { motion, useScroll, useTransform } from "framer-motion";
5
  import { useAuth } from "../../context/AuthContext";
 
90
  );
91
  }
92
 
93
+ export function TopBar({ gold, isMobile, onMenuClick }) {
94
  const { user, signOut } = useAuth();
95
  const location = useLocation();
96
  const time = useCurrentTime();
 
122
  }}
123
  role="banner"
124
  >
125
+ {/* Mobile hamburger + Brand */}
126
  <div
127
  style={{
128
  display: "flex",
 
135
  minWidth: isMobile ? 140 : 200,
136
  }}
137
  >
138
+ {isMobile && (
139
+ <button
140
+ onClick={onMenuClick}
141
+ aria-label="Open navigation"
142
+ style={{
143
+ background: "transparent",
144
+ border: "1px solid var(--border)",
145
+ borderRadius: "var(--radius-md)",
146
+ padding: "6px 7px",
147
+ color: "var(--text-muted)",
148
+ cursor: "pointer",
149
+ display: "flex",
150
+ alignItems: "center",
151
+ flexShrink: 0,
152
+ }}
153
+ >
154
+ <Menu size={15} />
155
+ </button>
156
+ )}
157
  <div
158
  style={{
159
  width: 30,