"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 { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Checkbox } from "@/components/ui/checkbox" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" 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 { buildSearchUrl, parseSearchParams, EMPTY_SEARCH_PARAMS, DATE_RANGE_OPTIONS, SEARCH_IN_OPTIONS, type SearchParams, } from "@/lib/mail-search/search-params" import { avatarColor, senderInitial } from "@/lib/sender-display" interface MailSearchBarProps { className?: string compact?: boolean } // ─── Advanced Search Panel ─────────────────────────────────────────────────── function AdvancedSearchPanel({ onClose, initialQuery, currentParams, }: { onClose: () => void initialQuery: string currentParams: SearchParams | null }) { const router = useRouter() const [from, setFrom] = useState(currentParams?.from ?? "") const [to, setTo] = useState(currentParams?.to ?? "") const [subject, setSubject] = useState(currentParams?.subject ?? "") const [hasWords, setHasWords] = useState( currentParams?.hasWords || currentParams?.q || initialQuery ) const [doesNotHave, setDoesNotHave] = useState(currentParams?.doesNotHave ?? "") const [sizeVal, setSizeVal] = useState(currentParams?.size ?? "") const [sizeOp, setSizeOp] = useState<"gt" | "lt">(currentParams?.sizeOp ?? "gt") const [sizeUnit, setSizeUnit] = useState<"Mo" | "Ko">(currentParams?.sizeUnit ?? "Mo") const [within, setWithin] = useState(currentParams?.within ?? "") const [dateAfter, setDateAfter] = useState(currentParams?.after ?? "") const [searchIn, setSearchIn] = useState(currentParams?.in ?? "all") const [hasAttachment, setHasAttachment] = useState( currentParams?.has?.includes("attachment") ?? false ) const [excludeChats, setExcludeChats] = useState(currentParams?.excludeChats ?? false) const handleSubmit = () => { const params: Partial = { ...EMPTY_SEARCH_PARAMS, q: "", from, to, subject, hasWords, doesNotHave, size: sizeVal, sizeOp, sizeUnit, within, after: dateAfter, in: searchIn, has: hasAttachment ? ["attachment"] : [], excludeChats, } router.push(buildSearchUrl(params)) onClose() } return (
setFrom(e.target.value)} className="h-8 flex-1 rounded border-0 border-b border-gray-300 bg-transparent px-1 text-sm shadow-none focus-visible:border-blue-500 focus-visible:ring-0 dark:border-gray-600" autoFocus />
setTo(e.target.value)} className="h-8 flex-1 rounded border-0 border-b border-gray-300 bg-transparent px-1 text-sm shadow-none focus-visible:border-blue-500 focus-visible:ring-0 dark:border-gray-600" />
setSubject(e.target.value)} className="h-8 flex-1 rounded border-0 border-b border-gray-300 bg-transparent px-1 text-sm shadow-none focus-visible:border-blue-500 focus-visible:ring-0 dark:border-gray-600" />
setHasWords(e.target.value)} className="h-8 flex-1 rounded border-0 border-b border-gray-300 bg-transparent px-1 text-sm shadow-none focus-visible:border-blue-500 focus-visible:ring-0 dark:border-gray-600" />
setDoesNotHave(e.target.value)} className="h-8 flex-1 rounded border-0 border-b border-gray-300 bg-transparent px-1 text-sm shadow-none focus-visible:border-blue-500 focus-visible:ring-0 dark:border-gray-600" />
setSizeVal(e.target.value)} className="h-8 w-20 rounded border-0 border-b border-gray-300 bg-transparent px-1 text-sm shadow-none focus-visible:border-blue-500 focus-visible:ring-0 dark:border-gray-600" />
setDateAfter(e.target.value)} className="h-8 min-w-0 flex-1 rounded border border-gray-300 bg-transparent px-2 text-sm shadow-none focus-visible:border-blue-500 focus-visible:ring-0 dark:border-gray-600" />
) } // ─── 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 if (!q.trim() && !chipAttachment && !chipLast7Days && !chipFromMe) return const params: Partial = { q: q.trim() } if (chipAttachment) params.has = ["attachment"] if (chipLast7Days) params.within = "1w" if (chipFromMe) params.from = account.email router.push(buildSearchUrl(params)) setDropdownOpen(false) inputRef.current?.blur() }, [inputValue, chipAttachment, chipLast7Days, chipFromMe, account.email, router] ) const selectSuggestion = useCallback( (s: SearchSuggestion) => { const params: Partial = { q: s.email } if (chipAttachment) params.has = ["attachment"] if (chipLast7Days) params.within = "1w" if (chipFromMe) params.from = account.email router.push(buildSearchUrl(params)) 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 outline-none placeholder:text-gray-500", 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} /> )}
) }