import { Search, X } from "@styled-icons/boxicons-regular"; import { HelpCircle } from "@styled-icons/boxicons-solid"; import styled from "styled-components/macro"; import { useEffect, useRef, useState } from "preact/hooks"; import { internalEmit } from "../../lib/eventEmitter"; import { useSearchAutoComplete, transformSearchQuery, UserMapping } from "../../lib/hooks/useSearchAutoComplete"; import SearchAutoComplete from "./SearchAutoComplete"; import SearchDatePicker from "./SearchDatePicker"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout"; import Tooltip from "../common/Tooltip"; const Container = styled.div` position: relative; display: flex; align-items: center; background: var(--primary-header); border-radius: var(--border-radius); width: 220px; height: 32px; @media (max-width: 768px) { width: 180px; } `; const Input = styled.input` flex: 1; border: none; background: transparent; color: var(--foreground); font-size: 14px; padding: 6px 2px 6px 8px; /* Reduced left padding for more space */ outline: none; min-width: 0; /* Allow input to shrink properly */ &::placeholder { color: var(--tertiary-foreground); font-size: 13px; /* Slightly smaller placeholder on mobile */ } @media (max-width: 768px) { font-size: 13px; padding: 6px 2px 6px 6px; } `; const IconButton = styled.div` display: flex; align-items: center; padding: 0 8px; /* Symmetrical padding */ color: var(--tertiary-foreground); cursor: pointer; transition: color 0.1s ease; flex-shrink: 0; /* Prevent icon from shrinking */ &:hover { color: var(--foreground); } @media (max-width: 768px) { padding: 0 6px; /* Less padding on mobile */ svg { width: 16px; height: 16px; } } `; const OptionsDropdown = styled.div` position: absolute; top: 100%; right: 0; margin-top: 8px; background: var(--primary-background); border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); z-index: 1000; overflow: hidden; padding: 8px; min-width: 300px; @media (max-width: 768px) { padding: 12px; min-width: 320px; max-width: calc(100vw - 20px); right: -10px; /* Adjust position to prevent edge cutoff */ max-height: 40vh; /* Limit height when keyboard is up */ overflow-y: auto; /* Make it scrollable */ /* Add scrollbar styles for mobile */ &::-webkit-scrollbar { width: 4px; } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 2px; } } `; const OptionsHeader = styled.div` padding: 0 8px 8px 8px; font-size: 12px; color: var(--tertiary-foreground); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; @media (max-width: 768px) { font-size: 11px; padding: 0 8px 8px 8px; } `; const Option = styled.div` display: flex; align-items: center; padding: 8px; border-radius: 4px; cursor: pointer; transition: background 0.15s ease; margin-bottom: 2px; &:hover { background: var(--secondary-background); } &:last-child { margin-bottom: 0; } @media (max-width: 768px) { padding: 10px 10px; margin-bottom: 3px; border-radius: 6px; /* Add touch feedback for mobile */ &:active { background: var(--secondary-background); transform: scale(0.98); } } `; const OptionLabel = styled.span` color: var(--foreground); font-weight: 500; margin-right: 8px; font-family: var(--monospace-font), monospace; font-size: 13px; white-space: nowrap; @media (max-width: 768px) { font-size: 13px; margin-right: 10px; } `; const OptionDesc = styled.span` color: var(--tertiary-foreground); font-size: 13px; flex: 1; @media (max-width: 768px) { font-size: 12px; } `; const HelpIcon = styled(HelpCircle)` color: var(--tertiary-foreground); margin-left: auto; flex-shrink: 0; opacity: 0; transition: opacity 0.15s ease; ${Option}:hover & { opacity: 0.7; } `; interface SearchOption { label: string; description: string; tooltip: string; } const searchOptions: SearchOption[] = [ { label: "from:", description: "user", tooltip: "Filter messages by author" }, { label: "mentions:", description: "user", tooltip: "Find messages mentioning a user" }, { label: "has:", description: "video, image, link, audio, file", tooltip: "Messages with specific attachment types" }, { label: "before:", description: "specific date", tooltip: "Messages before this date" }, { label: "during:", description: "specific date", tooltip: "Messages on this date" }, { label: "after:", description: "specific date", tooltip: "Messages after this date" }, { label: "between:", description: "date range", tooltip: "Messages between two dates" }, { label: "server-wide", description: "Entire server", tooltip: "Search in entire server instead of just this channel" } ]; export function SearchBar() { const layout = useApplicationState().layout; const [query, setQuery] = useState(""); const [showOptions, setShowOptions] = useState(false); const [isSearching, setIsSearching] = useState(false); const [userMappings, setUserMappings] = useState({}); const [showDatePicker, setShowDatePicker] = useState<"before" | "after" | "during" | "between" | null>(null); const [showAttachmentTypes, setShowAttachmentTypes] = useState(false); const [activeDateRange, setActiveDateRange] = useState<{ start: string; end: string; startDisplay: string; endDisplay: string; } | null>(null); const [showServerWideError, setShowServerWideError] = useState(false); const [showDateRangeError, setShowDateRangeError] = useState(false); const [showMultipleHasError, setShowMultipleHasError] = useState(false); const [showDuplicateFilterError, setShowDuplicateFilterError] = useState(false); const inputRef = useRef(null); const skipNextFocus = useRef(false); // Setup autocomplete const { state: autocompleteState, setState: setAutocompleteState, onKeyUp, onKeyDown: originalOnAutocompleteKeyDown, onChange: onAutocompleteChange, onClick: originalOnAutocompleteClick, onFocus: onAutocompleteFocus, onBlur: onAutocompleteBlur, } = useSearchAutoComplete(setQuery, userMappings, setUserMappings); // Wrap the autocomplete click to show options menu after selection const onAutocompleteClick = (userId: string, username: string) => { originalOnAutocompleteClick(userId, username); // Show options menu after user selection setTimeout(() => { setShowOptions(true); }, 100); }; // Wrap the onKeyDown handler to show options menu after Enter selection const onAutocompleteKeyDown = (e: KeyboardEvent) => { const handled = originalOnAutocompleteKeyDown(e); // If Enter or Tab was pressed and autocomplete was active, show options menu if (handled && (e.key === "Enter" || e.key === "Tab") && autocompleteState.type !== "none") { setTimeout(() => { setShowOptions(true); }, 100); } return handled; }; const handleFocus = () => { // Check if we should skip showing options this time if (skipNextFocus.current) { skipNextFocus.current = false; onAutocompleteFocus(); return; } onAutocompleteFocus(); // Don't show options if we're in the middle of typing a filter const hasIncompleteFilter = query.match(/\b(from:|mentions:|has:|before:|after:|during:|between:)\s*$/); if (!hasIncompleteFilter && !showDatePicker && !showAttachmentTypes && autocompleteState.type === "none") { setShowOptions(true); } }; const handleClick = () => { // Check if we're currently editing a filter const cursorPos = inputRef.current?.selectionStart || 0; const beforeCursor = query.slice(0, cursorPos); // Check if cursor is within a filter const isInFilter = beforeCursor.match(/\b(from:|mentions:|has:|before:|after:|during:|between:)[^\\s]*$/); // Show options when clicking on the input, if not editing a filter if (!isInFilter && !showOptions && autocompleteState.type === "none" && !showDatePicker && !showAttachmentTypes) { setShowOptions(true); } }; const handleBlur = () => { onAutocompleteBlur(); // Delay to allow clicking on options setTimeout(() => { // Check if we have an incomplete filter that should keep autocomplete open const hasUserFilter = query.match(/\b(from:|mentions:)[\s\S]*$/); const hasIncompleteDateFilter = query.match(/\b(before:|after:|during:)\s*$/); // Don't close options/autocomplete if we have a user filter if (!hasUserFilter && !hasIncompleteDateFilter && !showDatePicker) { setShowOptions(false); if (autocompleteState.type === "none") { setAutocompleteState({ type: "none" }); } } }, 200); }; const handleInput = (e: Event) => { const value = (e.target as HTMLInputElement).value; const cursorPos = (e.target as HTMLInputElement).selectionStart || 0; // Check if user is trying to add space after incomplete filter const incompleteFilterWithSpace = value.match(/\b(from:|mentions:)\s+$/); if (incompleteFilterWithSpace && autocompleteState.type === "user") { // Don't allow space after filter unless user was selected return; } setQuery(value); // Check for filters const beforeCursor = value.slice(0, cursorPos); // Check for user filters (from: and mentions:) - these should trigger autocomplete const fromMatch = beforeCursor.match(/\bfrom:[\s\S]*$/); const mentionsMatch = beforeCursor.match(/\bmentions:[\s\S]*$/); if (fromMatch || mentionsMatch) { // Close other dropdowns setShowOptions(false); setShowDatePicker(null); setShowAttachmentTypes(false); // Trigger autocomplete immediately for user filters onAutocompleteChange(e); return; } // Check for date filters const beforeMatch = beforeCursor.match(/\bbefore:\s*$/); const afterMatch = beforeCursor.match(/\bafter:\s*$/); const duringMatch = beforeCursor.match(/\bduring:\s*$/); const betweenMatch = beforeCursor.match(/\bbetween:\s*$/); const hasMatch = beforeCursor.match(/\bhas:\s*$/); if (beforeMatch) { setShowDatePicker("before"); setShowOptions(false); setShowAttachmentTypes(false); setAutocompleteState({ type: "none" }); } else if (afterMatch) { setShowDatePicker("after"); setShowOptions(false); setShowAttachmentTypes(false); setAutocompleteState({ type: "none" }); } else if (duringMatch) { setShowDatePicker("during"); setShowOptions(false); setShowAttachmentTypes(false); setAutocompleteState({ type: "none" }); } else if (betweenMatch) { setShowDatePicker("between"); setShowOptions(false); setShowAttachmentTypes(false); setAutocompleteState({ type: "none" }); } else if (hasMatch) { // Show attachment type options setShowAttachmentTypes(true); setShowOptions(false); setShowDatePicker(null); setAutocompleteState({ type: "none" }); } else { // Check if "has:" was removed and close attachment types dropdown if (showAttachmentTypes && !value.includes("has:")) { setShowAttachmentTypes(false); } // Only trigger autocomplete if no date/has filter is active const filterActive = value.match(/\b(before:|after:|during:|between:|has:)\s*$/); if (!filterActive) { onAutocompleteChange(e); if (showDatePicker) { setShowDatePicker(null); } } } }; const handleSearch = () => { let trimmedQuery = query.trim(); // Check for incomplete filters const hasIncompleteUserFilter = trimmedQuery.match(/\b(from:|mentions:)\s*$/); const hasIncompleteDateFilter = trimmedQuery.match(/\b(before:|after:|during:|between:)\s*$/); const hasIncompleteHasFilter = trimmedQuery.match(/\bhas:\s*$/); if (hasIncompleteUserFilter || hasIncompleteDateFilter || hasIncompleteHasFilter) { // Don't search if there's an incomplete filter return; } // Check for duplicate filters (only one of each type allowed) const checkDuplicates = (pattern: RegExp, filterName: string): boolean => { const matches = trimmedQuery.match(pattern) || []; if (matches.length > 1) { setShowDuplicateFilterError(true); setTimeout(() => setShowDuplicateFilterError(false), 3000); return true; } return false; }; // Check each filter type for duplicates if (checkDuplicates(/\bfrom:@?[\w-]+/gi, "from")) return; if (checkDuplicates(/\bmentions:@?[\w-]+/gi, "mentions")) return; if (checkDuplicates(/\bbefore:\d{4}-\d{2}-\d{2}/gi, "before")) return; if (checkDuplicates(/\bafter:\d{4}-\d{2}-\d{2}/gi, "after")) return; if (checkDuplicates(/\bduring:\d{4}-\d{2}-\d{2}/gi, "during")) return; if (checkDuplicates(/\bbetween:\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}/gi, "between")) return; // Check for multiple has: filters (only one attachment type filter allowed) const hasFilterMatches = trimmedQuery.match(/\bhas:(video|image|link|audio|file)/gi) || []; if (hasFilterMatches.length > 1) { // Show tooltip error message - only one attachment type filter is allowed setShowMultipleHasError(true); // Auto-hide after 3 seconds setTimeout(() => setShowMultipleHasError(false), 3000); return; } // Check for multiple date-range occurrences const dateRangeCount = (trimmedQuery.match(/\bdate-range\b/g) || []).length; if (dateRangeCount > 1) { // Show tooltip error message - only one date range is allowed setShowDateRangeError(true); // Auto-hide after 3 seconds setTimeout(() => setShowDateRangeError(false), 3000); return; } // Check if we have "date-range" in the query and replace it with actual dates if (dateRangeCount === 1 && activeDateRange) { // Replace "date-range" with the actual between filter for processing trimmedQuery = trimmedQuery.replace(/\bdate-range\b/, `between:${activeDateRange.start}..${activeDateRange.end}`); } // Transform query to use user IDs const searchParams = transformSearchQuery(trimmedQuery, userMappings); // Check if only server-wide is present without other filters if (searchParams.server_wide && !searchParams.query && !searchParams.author && !searchParams.mention && !searchParams.date_start && !searchParams.date_end && !searchParams.has) { // Show tooltip error message - server-wide requires other filters setShowServerWideError(true); // Auto-hide after 3 seconds setTimeout(() => setShowServerWideError(false), 3000); return; } // Check if we have any search criteria (query text or filters) // Allow empty query string if filters are present const hasFilters = searchParams.author || searchParams.mention || searchParams.date_start || searchParams.date_end || searchParams.has || searchParams.server_wide; const hasSearchCriteria = (searchParams.query && searchParams.query.trim() !== "") || hasFilters; if (hasSearchCriteria) { // Ensure the sidebar container is visible for desktop users if (!isTouchscreenDevice && !layout.getSectionState(SIDEBAR_MEMBERS, true)) { layout.setSectionState(SIDEBAR_MEMBERS, true, true); // Small delay to ensure the sidebar container is rendered first setTimeout(() => { internalEmit("RightSidebar", "open", "search", searchParams); }, 50); } else { // Sidebar is already visible, emit immediately internalEmit("RightSidebar", "open", "search", searchParams); } setShowOptions(false); setIsSearching(true); setAutocompleteState({ type: "none" }); inputRef.current?.blur(); // On mobile, automatically slide to show search results if (isTouchscreenDevice) { setTimeout(() => { const panels = document.querySelector("#app > div > div > div"); panels?.scrollTo({ behavior: "smooth", left: panels.clientWidth * 3, }); }, 100); // Small delay to ensure sidebar is opened first } } }; const handleKeyDown = (e: KeyboardEvent) => { const currentValue = (e.currentTarget as HTMLInputElement).value; const cursorPos = (e.currentTarget as HTMLInputElement).selectionStart || 0; // Handle backspace/delete for date filters and server-wide if (e.key === "Backspace" || e.key === "Delete") { const beforeCursor = currentValue.slice(0, cursorPos); const afterCursor = currentValue.slice(cursorPos); // Check for date filters with backspace if (e.key === "Backspace") { // Handle single date filters (before:, after:, during:) const singleDateMatch = beforeCursor.match(/\b(before|after|during):(\d{4}-\d{2}-\d{2})\s*$/); if (singleDateMatch) { e.preventDefault(); const filterStart = singleDateMatch.index!; // Remove the entire filter and date const newValue = currentValue.slice(0, filterStart) + afterCursor; setQuery(newValue); // Position cursor where filter was setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(filterStart, filterStart); } }, 0); return; } // Handle filter-only patterns (e.g., "before:" without date) const filterOnlyMatch = beforeCursor.match(/\b(before|after|during|between):\s*$/); if (filterOnlyMatch) { e.preventDefault(); const filterStart = filterOnlyMatch.index!; // Remove the entire filter const newValue = currentValue.slice(0, filterStart) + afterCursor; setQuery(newValue); // Close date picker if open setShowDatePicker(null); setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(filterStart, filterStart); } }, 0); return; } // Handle between date range filter const betweenMatch = beforeCursor.match(/\bbetween:(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})\s*$/); if (betweenMatch) { e.preventDefault(); const filterStart = betweenMatch.index!; const startDate = betweenMatch[1]; // Remove end date but keep "between:YYYY-MM-DD.." const newValue = currentValue.slice(0, filterStart) + "between:" + startDate + ".." + afterCursor; setQuery(newValue); const newCursorPos = filterStart + "between:".length + startDate.length + 2; setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(newCursorPos, newCursorPos); } }, 0); return; } // Handle partial between filter (between:YYYY-MM-DD..) const partialBetweenMatch = beforeCursor.match(/\bbetween:(\d{4}-\d{2}-\d{2})\.\.\s*$/); if (partialBetweenMatch) { e.preventDefault(); const filterStart = partialBetweenMatch.index!; const startDate = partialBetweenMatch[1]; // Remove ".." to get "between:YYYY-MM-DD" const newValue = currentValue.slice(0, filterStart) + "between:" + startDate + afterCursor; setQuery(newValue); const newCursorPos = filterStart + "between:".length + startDate.length; setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(newCursorPos, newCursorPos); } }, 0); return; } // Handle between with only start date (between:YYYY-MM-DD) const betweenStartOnlyMatch = beforeCursor.match(/\bbetween:(\d{4}-\d{2}-\d{2})\s*$/); if (betweenStartOnlyMatch) { e.preventDefault(); const filterStart = betweenStartOnlyMatch.index!; // Remove date but keep "between:" const newValue = currentValue.slice(0, filterStart) + "between:" + afterCursor; setQuery(newValue); const newCursorPos = filterStart + "between:".length; setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(newCursorPos, newCursorPos); // Open the date picker in between mode setShowDatePicker("between"); setShowOptions(false); } }, 0); return; } // Handle date-range keyword removal const dateRangeMatch = beforeCursor.match(/\bdate-range\s*$/); if (dateRangeMatch) { e.preventDefault(); const filterStart = dateRangeMatch.index!; // Remove the keyword and clear the stored date range const newValue = currentValue.slice(0, filterStart) + afterCursor; setQuery(newValue); setActiveDateRange(null); // Position cursor where filter was setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(filterStart, filterStart); } }, 0); return; } // Handle has: filter removal const hasMatch = beforeCursor.match(/\bhas:(video|image|link|audio|file)\s*$/); if (hasMatch) { e.preventDefault(); const filterStart = hasMatch.index!; // Remove the entire filter const newValue = currentValue.slice(0, filterStart) + afterCursor; setQuery(newValue); // Position cursor where filter was setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(filterStart, filterStart); } }, 0); return; } // Handle has: without value const hasOnlyMatch = beforeCursor.match(/\bhas:\s*$/); if (hasOnlyMatch) { e.preventDefault(); const filterStart = hasOnlyMatch.index!; // Remove the entire filter const newValue = currentValue.slice(0, filterStart) + afterCursor; setQuery(newValue); setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(filterStart, filterStart); } }, 0); return; } // Original server-wide handling const serverWideMatch = beforeCursor.match(/\bserver-wide\s*$/); if (serverWideMatch) { e.preventDefault(); const newValue = currentValue.slice(0, serverWideMatch.index) + afterCursor; setQuery(newValue); // Set cursor position to where server-wide started setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(serverWideMatch.index, serverWideMatch.index); } }, 0); return; } } else if (e.key === "Delete") { // Handle date filters with delete key // Check if we're at a filter: position const filterMatch = afterCursor.match(/^(before|after|during|between):(\d{4}-\d{2}-\d{2})?/); if (filterMatch && beforeCursor.match(/\s$|^$/)) { e.preventDefault(); const filterType = filterMatch[1]; const hasDate = filterMatch[2]; if (hasDate) { // Remove entire filter and date const newValue = beforeCursor + afterCursor.slice(filterMatch[0].length).trimStart(); setQuery(newValue); } else { // Just filter: without date, remove it const newValue = beforeCursor + afterCursor.slice(filterType.length + 1).trimStart(); setQuery(newValue); } setTimeout(() => { if (inputRef.current) { inputRef.current.setSelectionRange(beforeCursor.length, beforeCursor.length); } }, 0); return; } // Handle has: filter with delete key const hasAfter = afterCursor.match(/^has:(video|image|link|audio|file)?\s*/); if (hasAfter) { e.preventDefault(); const newValue = beforeCursor + afterCursor.slice(hasAfter[0].length); setQuery(newValue); return; } // Original server-wide handling const serverWideAfter = afterCursor.match(/^server-wide\s*/); if (serverWideAfter) { e.preventDefault(); const newValue = beforeCursor + afterCursor.slice(serverWideAfter[0].length); setQuery(newValue); return; } } } // Check if user is trying to type space when there's an incomplete filter if (e.key === " " || e.key === "Spacebar") { const beforeCursor = currentValue.slice(0, cursorPos); const afterCursor = currentValue.slice(cursorPos); // Check if cursor is within or right after a filter const lastFromIndex = beforeCursor.lastIndexOf("from:"); const lastMentionsIndex = beforeCursor.lastIndexOf("mentions:"); if (lastFromIndex !== -1 || lastMentionsIndex !== -1) { const filterIndex = Math.max(lastFromIndex, lastMentionsIndex); const afterFilter = beforeCursor.slice(filterIndex); // Check if we're still within the filter (no space after it in the part before cursor) if (!afterFilter.includes(" ")) { // Also check if there's no space immediately after cursor (editing in middle of username) const hasSpaceAfterCursor = afterCursor.startsWith(" "); if (!hasSpaceAfterCursor) { // We're within a filter and trying to add space - always prevent e.preventDefault(); return; } } } } // Let autocomplete handle key events first if (onAutocompleteKeyDown(e)) { return; } if (e.key === "Enter") { // Don't search if autocomplete is showing if (autocompleteState.type === "none") { handleSearch(); } } else if (e.key === "Escape") { if (query) { setQuery(""); } else { inputRef.current?.blur(); } if (isSearching) { internalEmit("RightSidebar", "close"); setIsSearching(false); } } }; const handleClear = () => { setQuery(""); setIsSearching(false); inputRef.current?.focus(); internalEmit("RightSidebar", "close"); }; const handleOptionClick = (option: SearchOption) => { // If it's a date filter, just show the date picker without adding text if (option.label === "before:" || option.label === "after:" || option.label === "during:" || option.label === "between:") { setShowDatePicker(option.label.slice(0, -1) as "before" | "after" | "during" | "between"); setShowOptions(false); // Ensure autocomplete is hidden setAutocompleteState({ type: "none" }); } else if (option.label === "has:") { // For has: filter, add it and show attachment type options const newQuery = query + (query ? " " : "") + "has:"; setQuery(newQuery); setShowOptions(false); setShowAttachmentTypes(true); // Ensure autocomplete is hidden setAutocompleteState({ type: "none" }); // Move cursor after "has:" setTimeout(() => { if (inputRef.current) { const endPos = newQuery.length; inputRef.current.setSelectionRange(endPos, endPos); inputRef.current.focus(); } }, 0); } else if (option.label === "server-wide") { // For server-wide, add it as a standalone filter with auto-space const newQuery = query + (query ? " " : "") + "server-wide "; setQuery(newQuery); setShowOptions(false); // Ensure autocomplete is hidden setAutocompleteState({ type: "none" }); // Set flag to skip showing options on the next focus skipNextFocus.current = true; // Move cursor to end after the space setTimeout(() => { if (inputRef.current) { const endPos = newQuery.length; inputRef.current.setSelectionRange(endPos, endPos); inputRef.current.focus(); } }, 0); } else if (option.label === "from:" || option.label === "mentions:") { // For user filters, add the text and let the user type to trigger autocomplete const newQuery = query + (query ? " " : "") + option.label; setQuery(newQuery); setShowOptions(false); // Focus and position cursor setTimeout(() => { if (inputRef.current) { const endPos = newQuery.length; inputRef.current.setSelectionRange(endPos, endPos); inputRef.current.focus(); } }, 0); } else { // For other filters, add the text immediately const newQuery = query + (query ? " " : "") + option.label; setQuery(newQuery); // Ensure autocomplete is hidden setAutocompleteState({ type: "none" }); inputRef.current?.focus(); } }; const handleAttachmentTypeClick = (type: string) => { // Add the attachment type to the query const newQuery = query + type + " "; setQuery(newQuery); setShowAttachmentTypes(false); // Move cursor to end and focus setTimeout(() => { if (inputRef.current) { const endPos = newQuery.length; inputRef.current.setSelectionRange(endPos, endPos); inputRef.current.focus(); } }, 0); }; // Format date to YYYY-MM-DD in local timezone const formatLocalDate = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const handleDateSelect = (date: Date) => { const dateStr = formatLocalDate(date); // Use local date formatting // Add the filter and date to the query with auto-space const filterText = `${showDatePicker}:${dateStr} `; const newQuery = query + (query ? " " : "") + filterText; setQuery(newQuery); setShowDatePicker(null); // Move cursor to end after the space setTimeout(() => { if (inputRef.current) { const endPos = newQuery.length; inputRef.current.setSelectionRange(endPos, endPos); inputRef.current.focus(); } }, 0); }; const handleRangeSelect = (startDate: Date, endDate: Date) => { const startStr = formatLocalDate(startDate); const endStr = formatLocalDate(endDate); // Store the actual date range in state setActiveDateRange({ start: startStr, end: endStr, startDisplay: startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), endDisplay: endDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) }); // Only add "date-range" to the query, not the actual dates const filterText = `date-range `; const newQuery = query + (query ? " " : "") + filterText; setQuery(newQuery); setShowDatePicker(null); // Move cursor to end after the space setTimeout(() => { if (inputRef.current) { const endPos = newQuery.length; inputRef.current.setSelectionRange(endPos, endPos); inputRef.current.focus(); } }, 0); }; // Global keyboard shortcut useEffect(() => { const handleGlobalKeydown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); inputRef.current?.focus(); } }; window.addEventListener("keydown", handleGlobalKeydown); return () => window.removeEventListener("keydown", handleGlobalKeydown); }, []); // Close date picker or attachment types when clicking outside useEffect(() => { if (showDatePicker || showAttachmentTypes) { const handleClickOutside = (e: MouseEvent) => { // Check if click is outside the container const container = e.target as HTMLElement; if (!container.closest('[data-search-container]') && !container.closest('[data-date-picker]')) { setShowDatePicker(null); setShowAttachmentTypes(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); } }, [showDatePicker, showAttachmentTypes]); return ( e.stopPropagation()}> {isSearching ? ( ) : ( )} {autocompleteState.type !== "none" && ( )} {showOptions && autocompleteState.type === "none" && !showDatePicker && !showAttachmentTypes && ( e.stopPropagation()}> {"Search Options"} {searchOptions.map((option) => ( ))} )} {showDatePicker && ( )} {showAttachmentTypes && ( e.stopPropagation()}> Attachment Types )} ); }