refactor(Search): improve search UX with loader timing and state management

Enhance the search experience by ensuring the loader is displayed for a minimum duration to avoid flickering. Additionally, introduce a `hasSearched` state to accurately display "No results found" only after a search has been performed.
This commit is contained in:
KevIsDev 2025-05-01 17:19:29 +01:00
parent fcaf8f66f0
commit b3e1048fa4

View File

@ -3,7 +3,6 @@ import type { TextSearchOptions, TextSearchOnProgressCallback, WebContainer } fr
import { workbenchStore } from '~/lib/stores/workbench'; import { workbenchStore } from '~/lib/stores/workbench';
import { webcontainer } from '~/lib/webcontainer'; import { webcontainer } from '~/lib/webcontainer';
import { WORK_DIR } from '~/utils/constants'; import { WORK_DIR } from '~/utils/constants';
import { Loader2 } from 'lucide-react';
import { debounce } from '~/utils/debounce'; import { debounce } from '~/utils/debounce';
interface DisplayMatch { interface DisplayMatch {
@ -94,6 +93,7 @@ export function Search() {
const [searchResults, setSearchResults] = useState<DisplayMatch[]>([]); const [searchResults, setSearchResults] = useState<DisplayMatch[]>([]);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [expandedFiles, setExpandedFiles] = useState<Record<string, boolean>>({}); const [expandedFiles, setExpandedFiles] = useState<Record<string, boolean>>({});
const [hasSearched, setHasSearched] = useState(false);
const groupedResults = useMemo(() => groupResultsByFile(searchResults), [searchResults]); const groupedResults = useMemo(() => groupResultsByFile(searchResults), [searchResults]);
@ -112,6 +112,7 @@ export function Search() {
setSearchResults([]); setSearchResults([]);
setIsSearching(false); setIsSearching(false);
setExpandedFiles({}); setExpandedFiles({});
setHasSearched(false);
return; return;
} }
@ -119,6 +120,10 @@ export function Search() {
setIsSearching(true); setIsSearching(true);
setSearchResults([]); setSearchResults([]);
setExpandedFiles({}); setExpandedFiles({});
setHasSearched(true);
const minLoaderTime = 300; // ms
const start = Date.now();
try { try {
const instance = await webcontainer; const instance = await webcontainer;
@ -144,8 +149,14 @@ export function Search() {
} catch (error) { } catch (error) {
console.error('Failed to initiate search:', error); console.error('Failed to initiate search:', error);
} finally { } finally {
const elapsed = Date.now() - start;
if (elapsed < minLoaderTime) {
setTimeout(() => setIsSearching(false), minLoaderTime - elapsed);
} else {
setIsSearching(false); setIsSearching(false);
} }
}
}, []); }, []);
const debouncedSearch = useCallback(debounce(handleSearch, 300), [handleSearch]); const debouncedSearch = useCallback(debounce(handleSearch, 300), [handleSearch]);
@ -178,10 +189,10 @@ export function Search() {
<div className="flex-1 overflow-auto py-2"> <div className="flex-1 overflow-auto py-2">
{isSearching && ( {isSearching && (
<div className="flex items-center justify-center h-32 text-bolt-elements-textTertiary"> <div className="flex items-center justify-center h-32 text-bolt-elements-textTertiary">
<Loader2 className="animate-spin mr-2" /> Searching... <div className="i-ph:circle-notch animate-spin mr-2" /> Searching...
</div> </div>
)} )}
{!isSearching && searchResults.length === 0 && ( {!isSearching && hasSearched && searchResults.length === 0 && searchQuery.trim() !== '' && (
<div className="flex items-center justify-center h-32 text-gray-500">No results found.</div> <div className="flex items-center justify-center h-32 text-gray-500">No results found.</div>
)} )}
{!isSearching && {!isSearching &&