1+ import { useState , useEffect , useRef } from 'react' ;
2+ import { useParams , Link , useSearchParams , useNavigate , useLocation } from 'react-router-dom' ;
13import { useAuth } from '../../context/AuthContext' ;
2- import { Menu } from 'lucide-react' ; // Import Menu Icon
4+ import { Menu , ChevronRight , Search } from 'lucide-react' ;
5+ import api from '../../utils/api' ;
36
4- function Header ( { onToggleSidebar, showToggle = true , children } ) { // Default showToggle to true
7+ function Header ( { onToggleSidebar, showToggle = true , isSidebarCollapsed = false } ) {
58 const { user } = useAuth ( ) ;
9+ const { projectId } = useParams ( ) ;
10+ const [ projectName , setProjectName ] = useState ( '' ) ;
11+ const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
12+ const navigate = useNavigate ( ) ;
13+ const location = useLocation ( ) ;
14+
15+ const [ inputValue , setInputValue ] = useState ( searchParams . get ( 'q' ) || '' ) ;
16+ const searchInputRef = useRef ( null ) ;
617 const initial = user ?. email ? user . email [ 0 ] . toUpperCase ( ) : 'D' ;
718
19+ useEffect ( ( ) => {
20+ let isMounted = true ;
21+ if ( ! projectId ) {
22+ queueMicrotask ( ( ) => {
23+ if ( isMounted ) setProjectName ( '' ) ;
24+ } ) ;
25+ return ;
26+ }
27+ api . get ( `/api/projects/${ projectId } ` )
28+ . then ( res => {
29+ if ( isMounted ) setProjectName ( res . data . name ) ;
30+ } )
31+ . catch ( err => console . error ( "Failed to fetch project name for header:" , err ) ) ;
32+ return ( ) => { isMounted = false ; } ;
33+ } , [ projectId ] ) ;
34+
35+ // Sync input value with URL search param
36+ useEffect ( ( ) => {
37+ let isMounted = true ;
38+ queueMicrotask ( ( ) => {
39+ if ( isMounted ) {
40+ setInputValue ( searchParams . get ( 'q' ) || '' ) ;
41+ }
42+ } ) ;
43+ return ( ) => { isMounted = false ; } ;
44+ } , [ searchParams ] ) ;
45+
46+ // Keyboard shortcut to focus search input
47+ useEffect ( ( ) => {
48+ const handleKeyPress = ( e ) => {
49+ if ( ( e . metaKey || e . ctrlKey ) && e . key === 'k' ) {
50+ e . preventDefault ( ) ;
51+ searchInputRef . current ?. focus ( ) ;
52+ }
53+ } ;
54+ window . addEventListener ( 'keydown' , handleKeyPress ) ;
55+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyPress ) ;
56+ } , [ ] ) ;
57+
58+ const handleSearchChange = ( e ) => {
59+ const val = e . target . value ;
60+ setInputValue ( val ) ;
61+ if ( location . pathname === '/dashboard' ) {
62+ setSearchParams ( val ? { q : val } : { } ) ;
63+ }
64+ } ;
65+
66+ const handleSearchKeyDown = ( e ) => {
67+ if ( e . key === 'Enter' ) {
68+ navigate ( `/dashboard?q=${ encodeURIComponent ( inputValue ) } ` ) ;
69+ }
70+ } ;
71+
872 return (
973 < header style = { {
1074 height : 'var(--header-height)' ,
@@ -13,17 +77,16 @@ function Header({ onToggleSidebar, showToggle = true, children }) { // Default s
1377 display : 'flex' ,
1478 alignItems : 'center' ,
1579 justifyContent : 'space-between' ,
16- padding : '0 1rem ' ,
80+ padding : '0 1.5rem ' ,
1781 position : 'fixed' ,
1882 top : 0 ,
1983 right : 0 ,
2084 left : 0 ,
2185 zIndex : 1000 ,
2286 width : '100%' ,
23- // Only add paddingLeft if we expect a sidebar (showToggle is a proxy for sidebar existence here)
24- // But wait, showToggle means "can we toggle it". Even if we can't toggle, if sidebar is gone, padding should be 0 (or standard).
25- // In Project Mode, showToggle is false. So paddingLeft should be 0 (or standard 1rem from padding above).
26- paddingLeft : showToggle ? 'calc(var(--sidebar-width) + 1rem)' : '1rem'
87+ paddingLeft : showToggle
88+ ? `calc(${ isSidebarCollapsed ? 'var(--sidebar-width-collapsed)' : 'var(--sidebar-width)' } + 1.5rem)`
89+ : '1.5rem'
2790 } } className = "responsive-header" >
2891
2992 { /* CSS override for mobile padding in style tag below */ }
@@ -35,35 +98,80 @@ function Header({ onToggleSidebar, showToggle = true, children }) { // Default s
3598 .mobile-toggle {
3699 display: block !important;
37100 }
101+ .search-container {
102+ display: none !important; /* Hide search bar on mobile headers to save space */
103+ }
38104 }
39105 .mobile-toggle {
40106 display: none;
41107 }
42108 ` } </ style >
43109
44110 { /* Mobile Menu Button - Only show if toggle is allowed */ }
45- { showToggle && (
46- < button
47- onClick = { onToggleSidebar }
48- className = "btn btn-ghost mobile-toggle"
49- style = { { padding : '8px' , color : 'var(--color-text-main)' } }
50- >
51- < Menu size = { 24 } />
52- </ button >
53- ) }
111+ < div style = { { display : 'flex' , alignItems : 'center' , gap : '0.75rem' } } >
112+ { showToggle && (
113+ < button
114+ onClick = { onToggleSidebar }
115+ className = "btn btn-ghost mobile-toggle"
116+ style = { { padding : '8px' , color : 'var(--color-text-main)' } }
117+ >
118+ < Menu size = { 20 } />
119+ </ button >
120+ ) }
121+
122+ { /* Breadcrumbs for Project View */ }
123+ { projectId && projectName && (
124+ < div style = { { display : 'flex' , alignItems : 'center' , gap : '8px' , fontSize : '0.8125rem' } } >
125+ < Link to = "/dashboard" style = { { color : 'var(--color-text-muted)' , textDecoration : 'none' } } >
126+ Personal
127+ </ Link >
128+ < ChevronRight size = { 14 } color = "var(--color-text-muted)" />
129+ < span style = { { fontWeight : 500 , color : 'var(--color-text-main)' } } >
130+ { projectName }
131+ </ span >
132+ </ div >
133+ ) }
134+ </ div >
54135
55- { /* Search / Center Content Slot */ }
56- < div style = { { flex : 1 , display : 'flex' , justifyContent : 'center' , padding : '0 2rem' } } >
57- { children }
136+ { /* Persistent Global Search Input */ }
137+ < div className = "search-container" style = { { flex : 1 , display : 'flex' , justifyContent : 'center' , padding : '0 2rem' } } >
138+ < div className = "auth-input-wrap" style = { { width : '100%' , maxWidth : '480px' , position : 'relative' } } >
139+ < Search size = { 15 } style = { { left : '12px' , position : 'absolute' , color : 'var(--color-text-muted)' , zIndex : 1 , top : '50%' , transform : 'translateY(-50%)' } } />
140+ < input
141+ ref = { searchInputRef }
142+ type = "text"
143+ className = "input-field"
144+ placeholder = "Search projects..."
145+ style = { { paddingLeft : '2.4rem' , paddingRight : '4rem' , height : '32px' , fontSize : '0.75rem' , background : 'var(--color-bg-input)' , border : '1px solid var(--color-border)' , borderRadius : '6px' } }
146+ value = { inputValue }
147+ onChange = { handleSearchChange }
148+ onKeyDown = { handleSearchKeyDown }
149+ />
150+ < div style = { {
151+ position : 'absolute' ,
152+ right : '8px' ,
153+ top : '50%' ,
154+ transform : 'translateY(-50%)' ,
155+ padding : '1px 5px' ,
156+ background : 'var(--color-bg-main)' ,
157+ border : '1px solid var(--color-border)' ,
158+ borderRadius : '4px' ,
159+ fontSize : '0.6rem' ,
160+ color : 'var(--color-text-muted)' ,
161+ pointerEvents : 'none'
162+ } } >
163+ { navigator . platform . includes ( 'Mac' ) ? '⌘ K' : 'Ctrl K' }
164+ </ div >
165+ </ div >
58166 </ div >
59167
60168 { /* User Profile */ }
61169 < div style = { { display : 'flex' , alignItems : 'center' , gap : '10px' } } >
62- < span style = { { fontSize : '0.85rem ' , color : 'var(--color-text-muted)' } } >
170+ < span style = { { fontSize : '0.8125rem ' , color : 'var(--color-text-muted)' } } >
63171 { user ?. email || 'Dev' }
64172 </ span >
65173 < div style = { {
66- width : '28px' , height : '28px' , borderRadius : '4px' ,
174+ width : '28px' , height : '28px' , borderRadius : '4px' ,
67175 background : 'var(--color-bg-input)' ,
68176 border : '1px solid var(--color-border)' ,
69177 color : 'var(--color-text-main)' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
0 commit comments