import { useState, useEffect, useMemo, useCallback } from "react"; import { View, Text, TouchableOpacity, FlatList, Platform, useWindowDimensions, ScrollView, } from "react-native"; import { Portal } from "react-native-paper"; import { Ionicons } from "@expo/vector-icons"; import { useLazyQuickSearchQuery, SearchResult } from "@/store/api/searchApi"; import { useAppSelector } from "@/store/hooks"; import { selectSidebarCollapsed } from "@/store/screen/screenSizeSlice"; interface SearchDropdownProps { searchQuery: string; onResultSelect: (result: SearchResult) => void; onViewAllResults: (query: string) => void; } const getIconName = (type: SearchResult["type"]) => { switch (type) { case "user": return "person-outline"; case "admin": return "shield-outline"; case "todo": return "list-outline"; default: return "search-outline"; } }; const getResultTypeLabel = (type: SearchResult["type"]) => { switch (type) { case "user": return "User"; case "admin": return "Admin"; case "todo": return "Todo"; default: return "Result"; } }; export const SearchDropdown = ({ searchQuery, onResultSelect, onViewAllResults, }: SearchDropdownProps) => { const [isVisible, setIsVisible] = useState(false); const containerId = "global-search-dropdown"; const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const sidebarCollapsed = useAppSelector(selectSidebarCollapsed); const [triggerSearch, { data, isLoading, error }] = useLazyQuickSearchQuery(); // Fetch results when query changes useEffect(() => { if (searchQuery && searchQuery.trim().length >= 2) { triggerSearch({ q: searchQuery }) .unwrap() .then(() => setIsVisible(true)) .catch(() => setIsVisible(false)); } else { setIsVisible(false); } }, [searchQuery, triggerSearch]); // Hide if query cleared useEffect(() => { if (!searchQuery.trim()) setIsVisible(false); }, [searchQuery]); // Close dropdown on outside click (web only) useEffect(() => { if (!isVisible || Platform.OS !== "web") return; const handler = (e: MouseEvent | TouchEvent) => { const el = document.getElementById(containerId); if (!el) return; if (e.target instanceof Node && !el.contains(e.target)) { setIsVisible(false); } }; document.addEventListener("mousedown", handler); document.addEventListener("touchstart", handler, { passive: true }); return () => { document.removeEventListener("mousedown", handler); document.removeEventListener("touchstart", handler); }; }, [isVisible]); // Group results memoized const groupedResults = useMemo(() => { if (!data?.results) return []; const groups: { type: string; items: SearchResult[] }[] = []; const map: Record = {}; data.results.forEach((r) => { (map[r.type] ||= []).push(r); }); for (const [type, items] of Object.entries(map)) { groups.push({ type, items }); } return groups; }, [data?.results]); const renderGroupHeader = useCallback( ({ type, items }: { type: string; items: SearchResult[] }) => ( {getResultTypeLabel(type as SearchResult["type"])}s ({items.length}) `${r.type}-${r.id}`} renderItem={({ item }) => ( { setIsVisible(false); onResultSelect(item); }} > {item.title} {item.subtitle ? ( {item.subtitle} ) : null} {item.description ? ( {item.description} ) : null} )} /> ), [onResultSelect] ); if (!isVisible || !searchQuery || searchQuery.length < 2) return null; // Calculate dropdown positioning based on sidebar and content area // Sidebar widths: collapsed = 80px, expanded = 200px // We want dropdown to span from sidebar edge to content area with padding const sidebarWidth = sidebarCollapsed ? 80 : 200; const contentPadding = 20; const rightPadding = 4; const leftOffset = sidebarWidth + contentPadding; const dropdownWidth = windowWidth - leftOffset - rightPadding - 16; // Calculate max height: leave space at bottom (48px from top + some bottom margin) const maxDropdownHeight = windowHeight - 48 - 32; // For empty results, make it narrower and align to the right const isEmpty = data && data.results.length === 0 && !isLoading; return ( {isLoading && ( Searching... )} {error && ( Search failed )} {isEmpty && ( No results found for "{searchQuery}" )} {groupedResults.length > 0 && ( g.type} renderItem={({ item }) => renderGroupHeader(item)} scrollEnabled={false} ListFooterComponent={ { setIsVisible(false); onViewAllResults(searchQuery); }} > View all results for "{searchQuery}" } /> )} ); };