Skip to content

Commit 997ba25

Browse files
chore: header component added (#68)
Closes #67
1 parent dc91d15 commit 997ba25

File tree

1 file changed

+208
-0
lines changed
  • src/quant_research_starter/frontend/cauweb/src/components

1 file changed

+208
-0
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { useState, useRef, useEffect } from 'react';
2+
import { Bell, Search, User, Settings, LogOut, ChevronDown, Menu, X } from 'lucide-react';
3+
4+
export const Header = () => {
5+
const [isProfileOpen, setIsProfileOpen] = useState(false);
6+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
7+
const [showSuggestions, setShowSuggestions] = useState(false);
8+
const [searchQuery, setSearchQuery] = useState('');
9+
10+
const profileRef = useRef<HTMLDivElement>(null);
11+
const searchRef = useRef<HTMLDivElement>(null);
12+
13+
// Close dropdowns when clicking outside
14+
useEffect(() => {
15+
const handleClickOutside = (event: MouseEvent) => {
16+
if (profileRef.current && !profileRef.current.contains(event.target as Node)) {
17+
setIsProfileOpen(false);
18+
}
19+
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
20+
setShowSuggestions(false);
21+
}
22+
};
23+
24+
document.addEventListener('mousedown', handleClickOutside);
25+
return () => document.removeEventListener('mousedown', handleClickOutside);
26+
}, []);
27+
28+
// Mock search suggestions
29+
const searchSuggestions = [
30+
"Mean Reversion Strategy",
31+
"BTC/USD Correlation",
32+
"Portfolio Optimization",
33+
"Risk Metrics Analysis",
34+
"Volatility Forecasting"
35+
];
36+
37+
const filteredSuggestions = searchSuggestions.filter(suggestion =>
38+
suggestion.toLowerCase().includes(searchQuery.toLowerCase())
39+
);
40+
41+
// Handle mobile search
42+
const handleMobileSearch = () => {
43+
// Implement mobile search modal or expandable search
44+
console.log("Mobile search activated");
45+
};
46+
47+
// Handle profile actions
48+
const handleProfileAction = (action: string) => {
49+
setIsProfileOpen(false);
50+
console.log(`${action} clicked`);
51+
// Add your navigation or action logic here
52+
};
53+
54+
return (
55+
<header className="bg-white dark:bg-gray-900 shadow-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-50">
56+
<div className="flex items-center justify-between px-4 lg:px-6 py-4">
57+
{/* Logo and Mobile Menu Button */}
58+
<div className="flex items-center space-x-4">
59+
{/* Mobile Menu Button */}
60+
<button
61+
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
62+
className="lg:hidden p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
63+
aria-label="Toggle menu"
64+
aria-expanded={isMobileMenuOpen}
65+
>
66+
{isMobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
67+
</button>
68+
69+
{/* Logo */}
70+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
71+
QuantResearch
72+
</h1>
73+
</div>
74+
75+
{/* Search Bar - Hidden on mobile, visible on desktop */}
76+
<div ref={searchRef} className="hidden lg:block flex-1 max-w-2xl mx-8">
77+
<div className="relative">
78+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
79+
<input
80+
type="text"
81+
value={searchQuery}
82+
onChange={(e) => {
83+
setSearchQuery(e.target.value);
84+
setShowSuggestions(true);
85+
}}
86+
onFocus={() => setShowSuggestions(true)}
87+
placeholder="Search strategies, assets, or metrics..."
88+
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-gray-50 dark:bg-gray-800 hover:bg-white dark:hover:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
89+
/>
90+
91+
{/* Search Suggestions Dropdown */}
92+
{showSuggestions && searchQuery && (
93+
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 max-h-60 overflow-y-auto">
94+
<div className="py-2">
95+
{filteredSuggestions.length > 0 ? (
96+
filteredSuggestions.map((suggestion, index) => (
97+
<button
98+
key={`suggestion-${index}`}
99+
onClick={() => {
100+
setSearchQuery(suggestion);
101+
setShowSuggestions(false);
102+
}}
103+
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
104+
>
105+
{suggestion}
106+
</button>
107+
))
108+
) : (
109+
<div className="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">
110+
No results found
111+
</div>
112+
)}
113+
</div>
114+
</div>
115+
)}
116+
</div>
117+
</div>
118+
119+
{/* Mobile Search Button */}
120+
<button
121+
onClick={handleMobileSearch}
122+
className="lg:hidden p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
123+
aria-label="Search"
124+
>
125+
<Search className="w-5 h-5" />
126+
</button>
127+
128+
{/* Right Section - Notifications & Profile */}
129+
<div className="flex items-center space-x-4">
130+
{/* Notifications - Hidden on mobile */}
131+
<button
132+
className="hidden sm:flex relative p-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
133+
aria-label="Notifications"
134+
>
135+
<Bell className="w-5 h-5" />
136+
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
137+
3
138+
</span>
139+
</button>
140+
141+
{/* Profile Dropdown */}
142+
<div ref={profileRef} className="relative">
143+
<button
144+
onClick={() => setIsProfileOpen(!isProfileOpen)}
145+
className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
146+
aria-label="User menu"
147+
aria-expanded={isProfileOpen}
148+
>
149+
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center shadow-sm">
150+
<User className="w-4 h-4 text-white" />
151+
</div>
152+
<div className="hidden sm:flex items-center space-x-1">
153+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
154+
Sarah Wilson
155+
</span>
156+
<ChevronDown
157+
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${
158+
isProfileOpen ? 'rotate-180' : ''
159+
}`}
160+
/>
161+
</div>
162+
</button>
163+
164+
{/* Dropdown Menu */}
165+
{isProfileOpen && (
166+
<div className="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
167+
<div className="py-1">
168+
{/* Mobile-only user name */}
169+
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700 sm:hidden">
170+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
171+
Sarah Wilson
172+
</span>
173+
</div>
174+
175+
<button
176+
onClick={() => handleProfileAction('Profile')}
177+
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150"
178+
>
179+
<User className="w-4 h-4 mr-3" />
180+
Profile
181+
</button>
182+
183+
<button
184+
onClick={() => handleProfileAction('Settings')}
185+
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150"
186+
>
187+
<Settings className="w-4 h-4 mr-3" />
188+
Settings
189+
</button>
190+
191+
<div className="border-t border-gray-200 dark:border-gray-700 my-1" />
192+
193+
<button
194+
onClick={() => handleProfileAction('Sign Out')}
195+
className="flex items-center w-full px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors duration-150"
196+
>
197+
<LogOut className="w-4 h-4 mr-3" />
198+
Sign Out
199+
</button>
200+
</div>
201+
</div>
202+
)}
203+
</div>
204+
</div>
205+
</div>
206+
</header>
207+
);
208+
};

0 commit comments

Comments
 (0)