import { ArrowRight, CircleHelp, LoaderCircle, Search, X } from 'lucide-react' import { useId, useMemo, useRef, useState } from 'react' import { applyMailSearchSuggestion, mailSearchSuggestions, validateMailSearchQuery, } from '#features/mail/lib/mail-search' import { cn } from '#shared/lib/utils' type MailSearchBarProps = { value: string activeQuery?: string onChange: (value: string) => void onSubmit: (value: string) => void | Promise } export function MailSearchBar({ value, activeQuery, onChange, onSubmit }: MailSearchBarProps) { const inputRef = useRef(null) const listboxId = useId() const messageId = useId() const [focused, setFocused] = useState(false) const [helpOpen, setHelpOpen] = useState(false) const [activeIndex, setActiveIndex] = useState(-1) const [touched, setTouched] = useState(false) const [submitting, setSubmitting] = useState(false) const [submissionError, setSubmissionError] = useState(false) const cursor = inputRef.current?.selectionStart ?? value.length const validation = useMemo(() => validateMailSearchQuery(value), [value]) const suggestions = useMemo(() => mailSearchSuggestions(value, cursor), [cursor, value]) const hasError = (touched && !validation.valid) || submissionError const isActiveQuery = validation.valid && Boolean(validation.query) && validation.query === activeQuery?.trim() const panelOpen = (hasError || (helpOpen && suggestions.length > 0) || (focused && value.trim().length > 0 && suggestions.length > 0)) && !submitting function setValue(nextValue: string) { onChange(nextValue) setTouched(false) setSubmissionError(false) setActiveIndex(-1) } function applySuggestion(suggestion: (typeof suggestions)[number]) { const next = applyMailSearchSuggestion(value, suggestion) setValue(next.value) setHelpOpen(false) requestAnimationFrame(() => { inputRef.current?.focus() inputRef.current?.setSelectionRange(next.cursor, next.cursor) }) } async function submitValue(nextValue: string) { const result = validateMailSearchQuery(nextValue) setTouched(true) if (!result.valid) return setHelpOpen(false) setActiveIndex(-1) setSubmissionError(false) setSubmitting(true) try { await onSubmit(result.query) } catch { setSubmissionError(true) } finally { setSubmitting(false) } } const state = submitting ? 'loading' : hasError ? 'error' : isActiveQuery ? 'success' : 'default' const message = submissionError ? "Search couldn't start. Please try again." : validation.valid ? isActiveQuery ? 'Showing results for this search.' : 'Search terms can match subjects, people, message text, and attachment names.' : validation.message return (
{ event.preventDefault() void submitValue(value) }} onBlur={(event) => { if (event.currentTarget.contains(event.relatedTarget)) return setFocused(false) setHelpOpen(false) if (value.trim()) setTouched(true) }} >
{message} {panelOpen ? (
{hasError ? (
{message}
) : ( <>
{value.trim() ? 'Continue your search' : 'Search with precision'} ↑↓ choose · Enter apply
{suggestions.map((suggestion, index) => (
event.preventDefault()} onMouseEnter={() => setActiveIndex(index)} onClick={() => applySuggestion(suggestion)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') applySuggestion(suggestion) }} > {suggestion.label} {suggestion.description}
))}
"phrase" exact OR either -term exclude ( ) group
)}
) : null}
) }