"use client" import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent, } from "react" import { useRouter, useSearchParams, usePathname } from "next/navigation" import { Search, SlidersHorizontal, X, Paperclip, Clock, User, } from "lucide-react" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { emails } from "@/lib/email-data" import { useContactsStore } from "@/lib/contacts/contacts-store" import { useActiveAccount } from "@/lib/stores/account-store" import { useMailSearchStore } from "@/lib/stores/mail-search-store" import { matchContacts, matchEmails, bestCompletion, type SearchSuggestion, } from "@/lib/mail-search/search-engine" import { parseSearchParams, } from "@/lib/mail-search/search-params" import { buildQuickSearchParams, submitMailSearch, } from "@/lib/mail-search/navigate" import { MAIL_SEARCH_SUGGESTIONS_DROPDOWN_CLASS } from "@/lib/mail-chrome-classes" import { avatarColor, senderInitial } from "@/lib/sender-display" interface MailSearchBarProps { className?: string compact?: boolean } import { AdvancedSearchPanel } from "@/components/gmail/mail-search/advanced-search-panel" // ─── Main Search Bar ───────────────────────────────────────────────────────── export function MailSearchBar({ className, compact = false, }: MailSearchBarProps) { const router = useRouter() const pathname = usePathname() const isOnSearchPage = pathname?.includes("/mail/search") ?? false const urlSearchParams = useSearchParams() const currentSearchParams = useMemo( () => parseSearchParams(urlSearchParams), [urlSearchParams] ) const account = useActiveAccount() const contacts = useContactsStore((s) => s.contacts) const inputValue = useMailSearchStore((s) => s.inputValue) const dropdownOpen = useMailSearchStore((s) => s.dropdownOpen) const selectedIndex = useMailSearchStore((s) => s.selectedIndex) const advancedOpen = useMailSearchStore((s) => s.advancedOpen) const chipAttachment = useMailSearchStore((s) => s.chipAttachment) const chipLast7Days = useMailSearchStore((s) => s.chipLast7Days) const chipFromMe = useMailSearchStore((s) => s.chipFromMe) const { setInputValue, setDropdownOpen, setSelectedIndex, setAdvancedOpen, toggleChipAttachment, toggleChipLast7Days, toggleChipFromMe, reset, } = useMailSearchStore.getState() const inputRef = useRef(null) const containerRef = useRef(null) const [focused, setFocused] = useState(false) useEffect(() => { const q = currentSearchParams?.q ?? "" if (q && !inputValue) { setInputValue(q) } }, [currentSearchParams?.q]) const suggestions = useMemo(() => { if (!inputValue.trim()) return [] const contactHits = matchContacts(inputValue, contacts, 5) const emailHits = matchEmails(inputValue, emails, 5) const seen = new Set(contactHits.map((c) => c.email)) const unique = emailHits.filter((e) => !seen.has(e.email)) return [...contactHits, ...unique] }, [inputValue, contacts]) const ghostText = useMemo( () => bestCompletion(inputValue, suggestions), [inputValue, suggestions] ) const totalItems = suggestions.length + 1 const submitSearch = useCallback( (overrideQuery?: string) => { const q = overrideQuery ?? inputValue const params = buildQuickSearchParams(q, { chipAttachment, chipLast7Days, chipFromMe, fromEmail: account.email, }) if (!Object.keys(params).length) return submitMailSearch(router, params, { onAfter: () => { setDropdownOpen(false) inputRef.current?.blur() }, }) }, [inputValue, chipAttachment, chipLast7Days, chipFromMe, account.email, router] ) const selectSuggestion = useCallback( (s: SearchSuggestion) => { const params = buildQuickSearchParams(s.email, { chipAttachment, chipLast7Days, chipFromMe, fromEmail: account.email, }) submitMailSearch(router, params, { onAfter: () => { setInputValue(s.email) setDropdownOpen(false) inputRef.current?.blur() }, }) }, [chipAttachment, chipLast7Days, chipFromMe, account.email, router] ) const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (!dropdownOpen && e.key !== "Enter") return switch (e.key) { case "ArrowDown": e.preventDefault() setSelectedIndex( selectedIndex < totalItems - 1 ? selectedIndex + 1 : 0 ) break case "ArrowUp": e.preventDefault() setSelectedIndex( selectedIndex > 0 ? selectedIndex - 1 : totalItems - 1 ) break case "Enter": e.preventDefault() if (selectedIndex >= 0 && selectedIndex < suggestions.length) { selectSuggestion(suggestions[selectedIndex]!) } else { submitSearch() } break case "Tab": if (ghostText) { e.preventDefault() setInputValue(inputValue + ghostText) } break case "Escape": e.preventDefault() setDropdownOpen(false) setAdvancedOpen(false) inputRef.current?.blur() break } }, [ dropdownOpen, selectedIndex, totalItems, suggestions, ghostText, inputValue, submitSearch, selectSuggestion, ] ) useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement | null if (!target) return if (containerRef.current?.contains(target)) return if (target.closest("[data-radix-popper-content-wrapper]")) return setDropdownOpen(false) setAdvancedOpen(false) } document.addEventListener("mousedown", handleClickOutside) return () => document.removeEventListener("mousedown", handleClickOutside) }, []) const showDropdown = dropdownOpen && inputValue.trim().length > 0 && focused return (
{/* Input row */}
{/* Ghost text overlay */} {ghostText && focused && (
{inputValue} {ghostText}
)} setInputValue(e.target.value)} onFocus={() => { setFocused(true) if (inputValue.trim()) setDropdownOpen(true) }} onBlur={() => setFocused(false)} onKeyDown={handleKeyDown} className={cn( "h-12 w-full rounded-full border-0 bg-muted text-sm text-foreground outline-none placeholder-shown:text-inherit placeholder:opacity-100", focused || advancedOpen ? "bg-white shadow-md ring-1 ring-gray-300 dark:bg-gray-900 dark:ring-gray-600" : "", compact ? "pl-11 pr-20" : "pl-11 pr-20" )} role="combobox" aria-expanded={showDropdown} aria-autocomplete="list" aria-controls="search-suggestions" autoComplete="off" /> {/* Clear button */} {inputValue && ( )} {/* Advanced settings button */}
{/* Dropdown */} {showDropdown && !advancedOpen && (
{/* Filter chips */}
{/* Suggestions */} {suggestions.map((s, i) => { const isSelected = i === selectedIndex if (s.kind === "contact") { const initial = senderInitial(s.displayName) const color = avatarColor(s.displayName) return ( ) } return ( ) })} {/* "All results" row */}
)} {/* Advanced search panel */} {advancedOpen && ( setAdvancedOpen(false)} initialQuery={inputValue} currentParams={currentSearchParams} /> )}
) }