11import { Input } from '@/components/ui/input' ;
22import { ThemeSelector } from '@/components/ThemeToggle' ;
3- import { Search } from 'lucide-react' ;
3+ import { Search , Heart , ArrowRight } from 'lucide-react' ;
44import { useDispatch , useSelector } from 'react-redux' ;
55import { selectAvatar , selectName } from '@/features/onboardingSelectors' ;
66import { clearSearch } from '@/features/searchSlice' ;
77import { convertFileSrc } from '@tauri-apps/api/core' ;
88import { FaceSearchDialog } from '@/components/Dialog/FaceSearchDialog' ;
9- import { Link } from 'react-router' ;
9+ import { Link , useNavigate } from 'react-router' ;
10+ import { useState , useRef , useEffect } from 'react' ;
11+ import { AnimatePresence , motion } from 'framer-motion' ;
12+ import { ROUTES } from '@/constants/routes' ;
13+ import { Separator } from '@/components/ui/separator' ;
14+ import { Avatar , AvatarFallback , AvatarImage } from '@/components/ui/avatar' ;
15+ import { RootState } from '@/app/store' ;
16+ import { usePictoQuery } from '@/hooks/useQueryExtension' ;
17+ import { fetchAllClusters } from '@/api/api-functions' ;
18+ import { setClusters } from '@/features/faceClustersSlice' ;
19+ import { Cluster } from '@/types/Media' ;
1020
1121export function Navbar ( ) {
1222 const userName = useSelector ( selectName ) ;
@@ -17,6 +27,56 @@ export function Navbar() {
1727 const queryImage = searchState . queryImage ;
1828
1929 const dispatch = useDispatch ( ) ;
30+ const navigate = useNavigate ( ) ;
31+
32+ const [ isExpanded , setIsExpanded ] = useState ( false ) ;
33+ const wrapperRef = useRef < HTMLDivElement > ( null ) ;
34+
35+ useEffect ( ( ) => {
36+ const handleOutsideClick = ( event : MouseEvent ) => {
37+ if (
38+ wrapperRef . current &&
39+ ! wrapperRef . current . contains ( event . target as Node )
40+ ) {
41+ setIsExpanded ( false ) ;
42+ }
43+ } ;
44+
45+ const handleKeyDown = ( event : KeyboardEvent ) => {
46+ if ( event . key === 'Escape' ) {
47+ setIsExpanded ( false ) ;
48+ }
49+ } ;
50+
51+ document . addEventListener ( 'mousedown' , handleOutsideClick ) ;
52+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
53+
54+ return ( ) => {
55+ document . removeEventListener ( 'mousedown' , handleOutsideClick ) ;
56+ document . removeEventListener ( 'keydown' , handleKeyDown ) ;
57+ } ;
58+ } , [ ] ) ;
59+
60+ const { clusters } = useSelector ( ( state : RootState ) => state . faceClusters ) ;
61+
62+ const { data : clustersData , isSuccess : clustersSuccess } = usePictoQuery ( {
63+ queryKey : [ 'clusters' ] ,
64+ queryFn : fetchAllClusters ,
65+ enabled : isExpanded && ( ! clusters || clusters . length === 0 ) ,
66+ } ) ;
67+
68+ useEffect ( ( ) => {
69+ if ( clustersSuccess && clustersData ?. data ?. clusters ) {
70+ const fetchedClusters = ( clustersData . data . clusters || [ ] ) as Cluster [ ] ;
71+ dispatch ( setClusters ( fetchedClusters ) ) ;
72+ }
73+ } , [ clustersData , clustersSuccess , dispatch ] ) ;
74+
75+ const handleNavigate = ( path : string ) => {
76+ navigate ( path ) ;
77+ setIsExpanded ( false ) ;
78+ } ;
79+
2080 return (
2181 < div className = "sticky top-0 z-40 flex h-14 w-full items-center justify-between border-b pr-4 backdrop-blur" >
2282 { /* Logo */ }
@@ -28,8 +88,11 @@ export function Navbar() {
2888 </ div >
2989
3090 { /* Search Bar */ }
31- < div className = "mx-auto flex max-w-md flex-1 justify-center px-4" >
32- < div className = "dark:bg-muted/50 flex w-full items-center gap-1 rounded-md bg-neutral-100 px-1 py-1" >
91+ < div className = "mx-auto flex max-w-lg flex-1 justify-center px-4" >
92+ < div
93+ ref = { wrapperRef }
94+ className = "dark:bg-muted/50 relative flex w-full items-center gap-1 rounded-md bg-neutral-100 px-1 py-1"
95+ >
3396 { /* Query Image */ }
3497 { queryImage && (
3598 < div className = "relative mr-2 ml-2" >
@@ -60,10 +123,11 @@ export function Navbar() {
60123 type = "search"
61124 placeholder = "Add to your search"
62125 className = "mr-2 flex-1 border-0 bg-neutral-200"
126+ onFocus = { ( ) => setIsExpanded ( true ) }
127+ onClick = { ( ) => setIsExpanded ( true ) }
63128 />
64129
65130 { /* FaceSearch Dialog */ }
66-
67131 < FaceSearchDialog />
68132
69133 < button
@@ -73,6 +137,81 @@ export function Navbar() {
73137 >
74138 < Search className = "h-4 w-4" />
75139 </ button >
140+
141+ < AnimatePresence >
142+ { isExpanded && (
143+ < motion . div
144+ initial = { { opacity : 0 , y : - 6 } }
145+ animate = { { opacity : 1 , y : 0 } }
146+ exit = { { opacity : 0 , y : - 6 } }
147+ transition = { { type : 'tween' , duration : 0.15 } }
148+ className = "border-border bg-popover absolute top-full right-0 left-0 z-50 mt-1 overflow-hidden rounded-lg border shadow-lg"
149+ >
150+ < Separator />
151+
152+ { /* FavoritesRow */ }
153+ < button
154+ type = "button"
155+ className = "hover:bg-accent flex w-full cursor-pointer items-center justify-between px-4 py-3 transition-colors"
156+ onClick = { ( ) => handleNavigate ( `/${ ROUTES . FAVOURITES } ` ) }
157+ >
158+ < div className = "flex items-center gap-2" >
159+ < Heart className = "h-4 w-4" />
160+ < span className = "text-sm font-medium" > Favourites</ span >
161+ </ div >
162+ </ button >
163+
164+ < Separator />
165+
166+ { /* FaceClustersRow */ }
167+ { clusters && clusters . length > 0 && (
168+ < div className = "flex flex-row flex-nowrap justify-between overflow-hidden px-4" >
169+ { clusters . slice ( 0 , 6 ) . map ( ( cluster : Cluster ) => (
170+ < button
171+ type = "button"
172+ key = { cluster . cluster_id }
173+ className = "hover:bg-accent flex cursor-pointer flex-col items-center gap-1 px-1 py-2 transition-opacity"
174+ onClick = { ( ) =>
175+ handleNavigate ( `/person/${ cluster . cluster_id } ` )
176+ }
177+ >
178+ < Avatar className = "border-border h-10 w-10 border" >
179+ < AvatarImage
180+ src = {
181+ cluster . face_image_base64
182+ ? `data:image/jpeg;base64,${ cluster . face_image_base64 } `
183+ : undefined
184+ }
185+ alt = { cluster . cluster_name || 'Person' }
186+ />
187+ < AvatarFallback >
188+ { cluster . cluster_name ?. charAt ( 0 ) . toUpperCase ( ) ||
189+ cluster . cluster_id . charAt ( 0 ) . toUpperCase ( ) }
190+ </ AvatarFallback >
191+ </ Avatar >
192+ < span className = "block max-w-[56px] truncate text-center text-xs" >
193+ { cluster . cluster_name ||
194+ `Person ${ cluster . cluster_id . slice ( - 4 ) } ` }
195+ </ span >
196+ </ button >
197+ ) ) }
198+ </ div >
199+ ) }
200+
201+ < Separator />
202+
203+ { /* SeeAllPeopleButton */ }
204+ < button
205+ type = "button"
206+ className = "text-muted-foreground hover:text-foreground hover:bg-accent flex w-full cursor-pointer items-center justify-center gap-1 px-4 py-2 text-sm transition-colors"
207+ onClick = { ( ) => handleNavigate ( `/${ ROUTES . AI } ` ) }
208+ >
209+ < ArrowRight className = "h-4 w-4" />
210+ < span > See all people</ span >
211+ </ button >
212+ </ motion . div >
213+ ) }
214+ </ AnimatePresence >
76215 </ div >
77216 </ div >
78217
0 commit comments