Skip to content

Commit 0d0cbec

Browse files
Merge pull request AOSSIE-Org#1314 from rohan-pandeyy/ui/navbar-vertical-expansion
Add expandable search dropdown with face clusters
2 parents c3b6722 + 678497f commit 0d0cbec

2 files changed

Lines changed: 447 additions & 5 deletions

File tree

frontend/src/components/Navigation/Navbar/Navbar.tsx

Lines changed: 144 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import { Input } from '@/components/ui/input';
22
import { ThemeSelector } from '@/components/ThemeToggle';
3-
import { Search } from 'lucide-react';
3+
import { Search, Heart, ArrowRight } from 'lucide-react';
44
import { useDispatch, useSelector } from 'react-redux';
55
import { selectAvatar, selectName } from '@/features/onboardingSelectors';
66
import { clearSearch } from '@/features/searchSlice';
77
import { convertFileSrc } from '@tauri-apps/api/core';
88
import { 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

1121
export 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

Comments
 (0)