1+ "use client" ;
2+
13import Link from "next/link" ;
24import menuLinks from "@/data/menu" ;
35import SocialSharing from "./SocialSharing" ;
46import { Sheet , SheetContent , SheetTrigger } from "./ui/sheet" ;
5- import { RiInstagramLine , RiTwitterFill , RiYoutubeFill , RiGithubFill , RiTiktokFill , RiPatreonFill } from "react-icons/ri" ;
7+ import {
8+ RiInstagramLine ,
9+ RiTwitterFill ,
10+ RiYoutubeFill ,
11+ RiGithubFill ,
12+ RiTiktokFill ,
13+ RiPatreonFill ,
14+ } from "react-icons/ri" ;
15+
16+ import { useState , useEffect , useRef } from "react" ;
17+ import { collection , getDocs , query , orderBy } from "firebase/firestore" ;
18+ import { db } from "@/lib/firebase" ;
19+
20+ interface SearchItem {
21+ id : string ;
22+ title : string ;
23+ slug : string ;
24+ img : string ;
25+ imgAlt : string ;
26+ }
627
728export default function Header ( ) {
29+ const [ allArticles , setAllArticles ] = useState < SearchItem [ ] > ( [ ] ) ;
30+ const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
31+ const [ suggestions , setSuggestions ] = useState < SearchItem [ ] > ( [ ] ) ;
32+ const [ isOpen , setIsOpen ] = useState ( false ) ;
33+ const containerRef = useRef < HTMLDivElement > ( null ) ;
34+
35+ // Fetch minimal article data on mount
36+ useEffect ( ( ) => {
37+ ( async ( ) => {
38+ const q = query ( collection ( db , "articles" ) , orderBy ( "date" , "desc" ) ) ;
39+ const snap = await getDocs ( q ) ;
40+ const items = snap . docs
41+ // only published
42+ . filter ( ( d ) => ( d . data ( ) as any ) . publish === true )
43+ . map ( ( d ) => {
44+ const data = d . data ( ) as any ;
45+ return {
46+ id : d . id ,
47+ title : data . title ,
48+ slug : data . slug ,
49+ img : data . img ,
50+ imgAlt : data . imgAlt || data . title ,
51+ } as SearchItem ;
52+ } ) ;
53+ setAllArticles ( items ) ;
54+ } ) ( ) ;
55+ } , [ ] ) ;
56+
57+ // update suggestions when searchTerm changes
58+ useEffect ( ( ) => {
59+ if ( searchTerm . length >= 2 ) {
60+ const term = searchTerm . toLowerCase ( ) ;
61+ setSuggestions (
62+ allArticles . filter ( ( a ) => a . title . toLowerCase ( ) . includes ( term ) )
63+ ) ;
64+ setIsOpen ( true ) ;
65+ } else {
66+ setIsOpen ( false ) ;
67+ setSuggestions ( [ ] ) ;
68+ }
69+ } , [ searchTerm , allArticles ] ) ;
70+
71+ // close dropdown when clicking outside
72+ useEffect ( ( ) => {
73+ const handleClick = ( e : MouseEvent ) => {
74+ if (
75+ containerRef . current &&
76+ ! containerRef . current . contains ( e . target as Node )
77+ ) {
78+ setIsOpen ( false ) ;
79+ }
80+ } ;
81+ document . addEventListener ( "click" , handleClick ) ;
82+ return ( ) => document . removeEventListener ( "click" , handleClick ) ;
83+ } , [ ] ) ;
84+
885 return (
986 < header className = "flex flex-col justify-between max-w-[95rem] w-full mx-auto px-4 md:pt-8 pt-4 lg:pb-4 md:pb-4 sm:pb-2 xs:pb-2" >
1087 < div className = "flex" >
88+ { /* Logo */ }
1189 < div className = "flex flex-1" >
1290 < Link href = "/" aria-label = "Return to homepage" >
1391 < img
@@ -17,8 +95,12 @@ export default function Header() {
1795 />
1896 </ Link >
1997 </ div >
98+
99+ { /* Mobile menu */ }
20100 < Sheet >
101+
21102 < SheetTrigger aria-labelledby = "button-label" >
103+
22104 < span id = "button-label" hidden >
23105 Menu
24106 </ span >
@@ -41,13 +123,10 @@ export default function Header() {
41123 className = "w-full pt-14"
42124 aria-label = "Menu Toggle"
43125 >
44- < nav
45- className = "flex flex-col flex-1 justify-end gap-6"
46- aria-labelledby = "mobile-nav"
47- >
48- { menuLinks . map ( ( menuItem , index ) => (
49- < Link key = { index } href = { menuItem . href } >
50- { menuItem . label }
126+ < nav className = "flex flex-col flex-1 justify-end gap-6" aria-labelledby = "mobile-nav" >
127+ { menuLinks . map ( ( m , i ) => (
128+ < Link key = { i } href = { m . href } >
129+ { m . label }
51130 </ Link >
52131 ) ) }
53132 < svg
@@ -88,21 +167,58 @@ export default function Header() {
88167 } ,
89168 {
90169 href : "http://patreon.com/lap_mgmt" ,
91- ariaLabel : "Visit our GitHub page" ,
170+ ariaLabel : "Visit our Patreon page" ,
92171 Icon : RiPatreonFill ,
93172 } ,
94173 ] }
95174 />
96175 </ nav >
97176 </ SheetContent >
98177 </ Sheet >
99- < nav
100- className = "flex-1 items-center justify-end gap-6 hidden md:flex"
101- aria-labelledby = "desktop-nav"
102- >
103- { menuLinks . map ( ( menuItem , index ) => (
104- < Link key = { index } href = { menuItem . href } className = "hover:text-[#8a2be2] transition ease-in-out duration-300" >
105- { menuItem . label }
178+
179+ { /* Desktop nav */ }
180+ < nav className = "hidden md:flex flex-1 items-center justify-end gap-6" aria-labelledby = "desktop-nav" >
181+ { /* Search box */ }
182+ < div ref = { containerRef } className = "relative ml-6 w-64" >
183+ < input
184+ type = "text"
185+ placeholder = "Search…"
186+ value = { searchTerm }
187+ onChange = { ( e ) => setSearchTerm ( e . target . value ) }
188+ className = "w-full px-3 py-2 bg-[#121212] text-white border border-white focus:outline-none"
189+ />
190+
191+ { isOpen && suggestions . length > 0 && (
192+ < ul className = "absolute right-0 mt-1 w-full bg-[#121212] border border-white/60 shadow-lg max-h-60 overflow-auto z-50" >
193+ { suggestions . map ( ( art ) => (
194+ < li
195+ key = { art . id }
196+ className = "flex items-center gap-2 px-3 py-2 hover:bg-[#892be250] transition"
197+ >
198+ < img
199+ src = { art . img }
200+ alt = { art . imgAlt }
201+ className = "w-10 h-10 object-cover rounded"
202+ />
203+ < Link
204+ href = { `/posts/${ art . slug } ` }
205+ onClick = { ( ) => setIsOpen ( false ) }
206+ className = "truncate text-white"
207+ >
208+ { art . title }
209+ </ Link >
210+ </ li >
211+ ) ) }
212+ </ ul >
213+ ) }
214+ </ div >
215+ { menuLinks . map ( ( m , i ) => (
216+ < Link
217+ key = { i }
218+ href = { m . href }
219+ className = "hover:text-[#8a2be2] transition ease-in-out duration-300"
220+ >
221+ { m . label }
106222 </ Link >
107223 ) ) }
108224 < svg
@@ -114,6 +230,7 @@ export default function Header() {
114230 >
115231 < rect width = "15" height = "1" fill = "white" />
116232 </ svg >
233+
117234 < SocialSharing
118235 links = { [
119236 {
@@ -143,13 +260,16 @@ export default function Header() {
143260 } ,
144261 {
145262 href : "http://patreon.com/lap_mgmt" ,
146- ariaLabel : "Visit our GitHub page" ,
263+ ariaLabel : "Visit our Patreon page" ,
147264 Icon : RiPatreonFill ,
148265 } ,
149266 ] }
150267 />
151268 </ nav >
269+
270+
152271 </ div >
272+
153273 < hr className = "border-white border-t-0 border mt-4" />
154274 </ header >
155275 ) ;
0 commit comments