From 88edbef5f72c4e0384ea7be22011308927b5a3e9 Mon Sep 17 00:00:00 2001 From: Vvictor-commits Date: Sat, 25 Apr 2026 21:27:57 +0100 Subject: [PATCH] feat: add creator search debounce utility - add useDebounce hook with flush support for immediate commit - wire 300ms debounce into LandingPage creator search - add onSubmit prop to SearchBar, fires on Enter to flush debounce immediately --- src/components/common/SearchBar.tsx | 3 +++ src/hooks/useDebounce.ts | 20 ++++++++++++++++++++ src/pages/LandingPage.tsx | 5 ++++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useDebounce.ts diff --git a/src/components/common/SearchBar.tsx b/src/components/common/SearchBar.tsx index cb6bd62..0e12319 100644 --- a/src/components/common/SearchBar.tsx +++ b/src/components/common/SearchBar.tsx @@ -5,6 +5,7 @@ import InlineValidationMessage from '@/components/common/InlineValidationMessage interface SearchBarProps { value: string; onChange: (value: string) => void; + onSubmit?: () => void; placeholder?: string; className?: string; validationMessage?: string; @@ -13,6 +14,7 @@ interface SearchBarProps { const SearchBar: React.FC = ({ value, onChange, + onSubmit, placeholder = 'Search creators by name or handle...', className, validationMessage, @@ -33,6 +35,7 @@ const SearchBar: React.FC = ({ placeholder={placeholder} value={value} onChange={e => onChange(e.target.value)} + onKeyDown={e => e.key === 'Enter' && onSubmit?.()} /> {validationMessage && ( diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..3bbe852 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,20 @@ +import { useEffect, useRef, useState } from 'react'; + +export function useDebounce(value: T, delay = 300): [T, (v: T) => void] { + const [debounced, setDebounced] = useState(value); + const timerRef = useRef | null>(null); + + useEffect(() => { + timerRef.current = setTimeout(() => setDebounced(value), delay); + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, [value, delay]); + + const flush = (v: T) => { + if (timerRef.current) clearTimeout(timerRef.current); + setDebounced(v); + }; + + return [debounced, flush]; +} diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index be3210d..a4333bc 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; +import { useDebounce } from '@/hooks/useDebounce'; import { courseService, type Course } from '@/services/course.service'; import SearchBar from '@/components/common/SearchBar'; import StickyFilterBar from '@/components/common/StickyFilterBar'; @@ -129,6 +130,7 @@ function LandingPage() { const { isMismatch: isNetworkMismatch } = useNetworkMismatch(); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); + const [debouncedSearchQuery, flushSearchQuery] = useDebounce(searchQuery, 300); const [activeProfileTab, setActiveProfileTab] = useState('overview'); const [featuredHoldings, setFeaturedHoldings] = useState(3); const [tradeSide, setTradeSide] = useState('buy'); @@ -151,7 +153,7 @@ function LandingPage() { }); const pendingScrollRestoreRef = useRef(null); - const trimmedSearchQuery = searchQuery.trim(); + const trimmedSearchQuery = debouncedSearchQuery.trim(); const hasInvalidSearchInput = /[^a-zA-Z0-9_\s-]/.test(trimmedSearchQuery); const searchValidationMessage = hasInvalidSearchInput ? 'Only letters, numbers, spaces, hyphens, and underscores are supported.' @@ -372,6 +374,7 @@ function LandingPage() { flushSearchQuery(searchQuery)} validationMessage={searchValidationMessage} className="max-w-none shadow-2xl shadow-black/20" />