refactor: useScheduleSearch 훅 분리 - 검색 로직 캡슐화
- useScheduleSearch.js 생성 (217줄) - 검색어 자동완성 API 호출 (debounce) - 무한 스크롤 검색 결과 (useInfiniteQuery) - 키보드 네비게이션 핸들러 - Schedules.jsx: 1139줄 → 1009줄 (130줄 감소) - 검색 관련 상태/로직을 훅으로 이동 - 컴포넌트 복잡도 감소 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
81a2112b59
commit
08d704da5c
3 changed files with 251 additions and 161 deletions
|
|
@ -1,2 +1,5 @@
|
||||||
// 관리자 인증
|
// 관리자 인증
|
||||||
export { useAdminAuth, useRedirectIfAuthenticated } from './useAdminAuth';
|
export { useAdminAuth, useRedirectIfAuthenticated } from './useAdminAuth';
|
||||||
|
|
||||||
|
// 일정 검색
|
||||||
|
export { useScheduleSearch } from './useScheduleSearch';
|
||||||
|
|
|
||||||
217
frontend-temp/src/hooks/pc/admin/useScheduleSearch.js
Normal file
217
frontend-temp/src/hooks/pc/admin/useScheduleSearch.js
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
/**
|
||||||
|
* 일정 검색 관련 커스텀 훅
|
||||||
|
* - 검색어 자동완성
|
||||||
|
* - 무한 스크롤 검색 결과
|
||||||
|
*/
|
||||||
|
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||||
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import useScheduleStore from '@/stores/useScheduleStore';
|
||||||
|
import * as schedulesApi from '@/api/admin/schedules';
|
||||||
|
|
||||||
|
const SEARCH_LIMIT = 20;
|
||||||
|
|
||||||
|
export function useScheduleSearch() {
|
||||||
|
// Zustand 스토어에서 검색 상태 가져오기
|
||||||
|
const {
|
||||||
|
searchInput,
|
||||||
|
setSearchInput,
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
isSearchMode,
|
||||||
|
setIsSearchMode,
|
||||||
|
} = useScheduleStore();
|
||||||
|
|
||||||
|
// 검색 추천 관련 상태
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
||||||
|
const [originalSearchQuery, setOriginalSearchQuery] = useState('');
|
||||||
|
const [suggestions, setSuggestions] = useState([]);
|
||||||
|
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
||||||
|
|
||||||
|
// Intersection Observer for infinite scroll
|
||||||
|
const { ref: loadMoreRef, inView } = useInView({
|
||||||
|
threshold: 0,
|
||||||
|
rootMargin: '100px',
|
||||||
|
});
|
||||||
|
|
||||||
|
// useInfiniteQuery for search
|
||||||
|
const {
|
||||||
|
data: searchData,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading: searchLoading,
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['adminScheduleSearch', searchTerm],
|
||||||
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
|
return schedulesApi.searchSchedules(searchTerm, { offset: pageParam, limit: SEARCH_LIMIT });
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
if (lastPage.hasMore) {
|
||||||
|
return lastPage.offset + lastPage.schedules.length;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
enabled: !!searchTerm && isSearchMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flatten search results
|
||||||
|
const searchResults = useMemo(() => {
|
||||||
|
if (!searchData?.pages) return [];
|
||||||
|
return searchData.pages.flatMap((page) => page.schedules);
|
||||||
|
}, [searchData]);
|
||||||
|
|
||||||
|
// Auto fetch next page when scrolled to bottom
|
||||||
|
const prevInViewRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView && !prevInViewRef.current && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
prevInViewRef.current = inView;
|
||||||
|
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]);
|
||||||
|
|
||||||
|
// 검색어 자동완성 API 호출 (debounce 적용)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!originalSearchQuery || originalSearchQuery.trim().length === 0) {
|
||||||
|
setSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(async () => {
|
||||||
|
setIsLoadingSuggestions(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/schedules/suggestions?q=${encodeURIComponent(originalSearchQuery)}&limit=10`
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setSuggestions(data.suggestions || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('추천 검색어 API 오류:', error);
|
||||||
|
setSuggestions([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSuggestions(false);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [originalSearchQuery]);
|
||||||
|
|
||||||
|
// 검색 실행
|
||||||
|
const handleSearch = useCallback((query) => {
|
||||||
|
const trimmedQuery = query?.trim() || '';
|
||||||
|
if (trimmedQuery) {
|
||||||
|
setSearchTerm(trimmedQuery);
|
||||||
|
setIsSearchMode(true);
|
||||||
|
}
|
||||||
|
setShowSuggestions(false);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
}, [setSearchTerm, setIsSearchMode]);
|
||||||
|
|
||||||
|
// 검색 모드 종료
|
||||||
|
const exitSearchMode = useCallback(() => {
|
||||||
|
setIsSearchMode(false);
|
||||||
|
setSearchTerm('');
|
||||||
|
setSearchInput('');
|
||||||
|
setShowSuggestions(false);
|
||||||
|
setSuggestions([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
setOriginalSearchQuery('');
|
||||||
|
}, [setIsSearchMode, setSearchTerm, setSearchInput]);
|
||||||
|
|
||||||
|
// 검색어 입력 핸들러
|
||||||
|
const handleSearchInputChange = useCallback((value) => {
|
||||||
|
setSearchInput(value);
|
||||||
|
setOriginalSearchQuery(value);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
if (value.trim()) {
|
||||||
|
setShowSuggestions(true);
|
||||||
|
} else {
|
||||||
|
setShowSuggestions(false);
|
||||||
|
}
|
||||||
|
}, [setSearchInput]);
|
||||||
|
|
||||||
|
// 추천 검색어 선택
|
||||||
|
const handleSuggestionSelect = useCallback((suggestion) => {
|
||||||
|
setSearchInput(suggestion);
|
||||||
|
handleSearch(suggestion);
|
||||||
|
}, [setSearchInput, handleSearch]);
|
||||||
|
|
||||||
|
// 키보드 네비게이션 핸들러
|
||||||
|
const handleKeyDown = useCallback((e) => {
|
||||||
|
if (!showSuggestions || suggestions.length === 0) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSearch(searchInput);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedSuggestionIndex((prev) => {
|
||||||
|
const next = prev < suggestions.length - 1 ? prev + 1 : prev;
|
||||||
|
if (next >= 0 && suggestions[next]) {
|
||||||
|
setSearchInput(suggestions[next]);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedSuggestionIndex((prev) => {
|
||||||
|
const next = prev > 0 ? prev - 1 : -1;
|
||||||
|
if (next === -1) {
|
||||||
|
setSearchInput(originalSearchQuery);
|
||||||
|
} else if (suggestions[next]) {
|
||||||
|
setSearchInput(suggestions[next]);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault();
|
||||||
|
if (selectedSuggestionIndex >= 0 && suggestions[selectedSuggestionIndex]) {
|
||||||
|
handleSuggestionSelect(suggestions[selectedSuggestionIndex]);
|
||||||
|
} else {
|
||||||
|
handleSearch(searchInput);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
setShowSuggestions(false);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [showSuggestions, suggestions, selectedSuggestionIndex, searchInput, originalSearchQuery, handleSearch, handleSuggestionSelect, setSearchInput]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 상태
|
||||||
|
searchInput,
|
||||||
|
searchTerm,
|
||||||
|
isSearchMode,
|
||||||
|
setIsSearchMode,
|
||||||
|
showSuggestions,
|
||||||
|
setShowSuggestions,
|
||||||
|
selectedSuggestionIndex,
|
||||||
|
suggestions,
|
||||||
|
isLoadingSuggestions,
|
||||||
|
searchResults,
|
||||||
|
searchLoading,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
|
||||||
|
// 핸들러
|
||||||
|
handleSearch,
|
||||||
|
exitSearchMode,
|
||||||
|
handleSearchInputChange,
|
||||||
|
handleSuggestionSelect,
|
||||||
|
handleKeyDown,
|
||||||
|
|
||||||
|
// refs
|
||||||
|
loadMoreRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useScheduleSearch;
|
||||||
|
|
@ -14,15 +14,14 @@ import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Book,
|
Book,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useInView } from 'react-intersection-observer';
|
|
||||||
|
|
||||||
import { Toast } from '@/components/common';
|
import { Toast } from '@/components/common';
|
||||||
import { AdminLayout, ConfirmDialog } from '@/components/pc/admin';
|
import { AdminLayout, ConfirmDialog } from '@/components/pc/admin';
|
||||||
import { ScheduleItem, getEditPath } from '@/components/pc/admin/schedule';
|
import { ScheduleItem, getEditPath } from '@/components/pc/admin/schedule';
|
||||||
import useScheduleStore from '@/stores/useScheduleStore';
|
import useScheduleStore from '@/stores/useScheduleStore';
|
||||||
import { useAdminAuth } from '@/hooks/pc/admin';
|
import { useAdminAuth, useScheduleSearch } from '@/hooks/pc/admin';
|
||||||
import { useToast } from '@/hooks/common';
|
import { useToast } from '@/hooks/common';
|
||||||
import { getTodayKST, formatDate } from '@/utils';
|
import { getTodayKST, formatDate } from '@/utils';
|
||||||
import { getCategoryId, getScheduleDate } from '@/utils/schedule';
|
import { getCategoryId, getScheduleDate } from '@/utils/schedule';
|
||||||
|
|
@ -33,14 +32,8 @@ function Schedules() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Zustand 스토어에서 상태 가져오기
|
// Zustand 스토어에서 상태 가져오기 (검색 제외)
|
||||||
const {
|
const {
|
||||||
searchInput,
|
|
||||||
setSearchInput,
|
|
||||||
searchTerm,
|
|
||||||
setSearchTerm,
|
|
||||||
isSearchMode,
|
|
||||||
setIsSearchMode,
|
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
setSelectedCategories,
|
setSelectedCategories,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
|
|
@ -53,95 +46,36 @@ function Schedules() {
|
||||||
|
|
||||||
const { user, isAuthenticated } = useAdminAuth();
|
const { user, isAuthenticated } = useAdminAuth();
|
||||||
|
|
||||||
|
// 검색 관련 (커스텀 훅)
|
||||||
|
const {
|
||||||
|
searchInput,
|
||||||
|
searchTerm,
|
||||||
|
isSearchMode,
|
||||||
|
setIsSearchMode,
|
||||||
|
showSuggestions,
|
||||||
|
setShowSuggestions,
|
||||||
|
selectedSuggestionIndex,
|
||||||
|
suggestions,
|
||||||
|
isLoadingSuggestions,
|
||||||
|
searchResults,
|
||||||
|
searchLoading,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
handleSearch,
|
||||||
|
exitSearchMode,
|
||||||
|
handleSearchInputChange,
|
||||||
|
handleSuggestionSelect,
|
||||||
|
handleKeyDown: handleSearchKeyDown,
|
||||||
|
loadMoreRef,
|
||||||
|
} = useScheduleSearch();
|
||||||
|
|
||||||
// 로컬 상태 (페이지 이동 시 유지할 필요 없는 것들)
|
// 로컬 상태 (페이지 이동 시 유지할 필요 없는 것들)
|
||||||
const { toast, setToast } = useToast();
|
const { toast, setToast } = useToast();
|
||||||
const scrollContainerRef = useRef(null);
|
const scrollContainerRef = useRef(null);
|
||||||
const searchContainerRef = useRef(null); // 검색 컨테이너 (외부 클릭 감지용)
|
const searchContainerRef = useRef(null); // 검색 컨테이너 (외부 클릭 감지용)
|
||||||
|
|
||||||
// 검색 추천 관련 상태
|
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
||||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
|
||||||
const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // 필터링용 원본 쿼리
|
|
||||||
const [suggestions, setSuggestions] = useState([]); // 추천 검색어 목록
|
|
||||||
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
|
||||||
|
|
||||||
const SEARCH_LIMIT = 20; // 페이지당 20개
|
|
||||||
const ESTIMATED_ITEM_HEIGHT = 100; // 아이템 추정 높이 (동적 측정)
|
const ESTIMATED_ITEM_HEIGHT = 100; // 아이템 추정 높이 (동적 측정)
|
||||||
|
|
||||||
// Intersection Observer for infinite scroll
|
|
||||||
const { ref: loadMoreRef, inView } = useInView({
|
|
||||||
threshold: 0,
|
|
||||||
rootMargin: '100px',
|
|
||||||
});
|
|
||||||
|
|
||||||
// useInfiniteQuery for search
|
|
||||||
const {
|
|
||||||
data: searchData,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
isLoading: searchLoading,
|
|
||||||
} = useInfiniteQuery({
|
|
||||||
queryKey: ['adminScheduleSearch', searchTerm],
|
|
||||||
queryFn: async ({ pageParam = 0 }) => {
|
|
||||||
return schedulesApi.searchSchedules(searchTerm, { offset: pageParam, limit: SEARCH_LIMIT });
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
if (lastPage.hasMore) {
|
|
||||||
return lastPage.offset + lastPage.schedules.length;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
enabled: !!searchTerm && isSearchMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Flatten search results
|
|
||||||
const searchResults = useMemo(() => {
|
|
||||||
if (!searchData?.pages) return [];
|
|
||||||
return searchData.pages.flatMap((page) => page.schedules);
|
|
||||||
}, [searchData]);
|
|
||||||
|
|
||||||
// Auto fetch next page when scrolled to bottom
|
|
||||||
// inView가 true로 변경될 때만 fetch (중복 요청 방지)
|
|
||||||
const prevInViewRef = useRef(false);
|
|
||||||
useEffect(() => {
|
|
||||||
// inView가 false→true로 변경될 때만 fetch
|
|
||||||
if (inView && !prevInViewRef.current && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) {
|
|
||||||
fetchNextPage();
|
|
||||||
}
|
|
||||||
prevInViewRef.current = inView;
|
|
||||||
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]);
|
|
||||||
|
|
||||||
// 검색어 자동완성 API 호출 (debounce 적용)
|
|
||||||
useEffect(() => {
|
|
||||||
// 검색어가 비어있으면 초기화
|
|
||||||
if (!originalSearchQuery || originalSearchQuery.trim().length === 0) {
|
|
||||||
setSuggestions([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// debounce: 200ms 후에 API 호출
|
|
||||||
const timeoutId = setTimeout(async () => {
|
|
||||||
setIsLoadingSuggestions(true);
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/schedules/suggestions?q=${encodeURIComponent(originalSearchQuery)}&limit=10`
|
|
||||||
);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setSuggestions(data.suggestions || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('추천 검색어 API 오류:', error);
|
|
||||||
setSuggestions([]);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingSuggestions(false);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [originalSearchQuery]);
|
|
||||||
|
|
||||||
// selectedDate가 없으면 오늘 날짜로 초기화
|
// selectedDate가 없으면 오늘 날짜로 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedDate) {
|
if (!selectedDate) {
|
||||||
|
|
@ -843,14 +777,7 @@ function Schedules() {
|
||||||
className="flex items-center gap-3 flex-1"
|
className="flex items-center gap-3 flex-1"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={exitSearchMode}
|
||||||
setIsSearchMode(false);
|
|
||||||
setSearchInput('');
|
|
||||||
setOriginalSearchQuery('');
|
|
||||||
setSearchTerm('');
|
|
||||||
setShowSuggestions(false);
|
|
||||||
setSelectedSuggestionIndex(-1);
|
|
||||||
}}
|
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} className="text-gray-500" />
|
<ArrowLeft size={20} className="text-gray-500" />
|
||||||
|
|
@ -864,65 +791,13 @@ function Schedules() {
|
||||||
placeholder="일정 검색..."
|
placeholder="일정 검색..."
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(e) => {
|
onChange={(e) => handleSearchInputChange(e.target.value)}
|
||||||
setSearchInput(e.target.value);
|
|
||||||
setOriginalSearchQuery(e.target.value);
|
|
||||||
setShowSuggestions(true);
|
|
||||||
setSelectedSuggestionIndex(-1);
|
|
||||||
}}
|
|
||||||
onFocus={() => setShowSuggestions(true)}
|
onFocus={() => setShowSuggestions(true)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={handleSearchKeyDown}
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
const newIndex =
|
|
||||||
selectedSuggestionIndex < suggestions.length - 1
|
|
||||||
? selectedSuggestionIndex + 1
|
|
||||||
: 0;
|
|
||||||
setSelectedSuggestionIndex(newIndex);
|
|
||||||
if (suggestions[newIndex]) {
|
|
||||||
setSearchInput(suggestions[newIndex]);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
const newIndex =
|
|
||||||
selectedSuggestionIndex > 0
|
|
||||||
? selectedSuggestionIndex - 1
|
|
||||||
: suggestions.length - 1;
|
|
||||||
setSelectedSuggestionIndex(newIndex);
|
|
||||||
if (suggestions[newIndex]) {
|
|
||||||
setSearchInput(suggestions[newIndex]);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
if (
|
|
||||||
selectedSuggestionIndex >= 0 &&
|
|
||||||
suggestions[selectedSuggestionIndex]
|
|
||||||
) {
|
|
||||||
setSearchInput(suggestions[selectedSuggestionIndex]);
|
|
||||||
setSearchTerm(suggestions[selectedSuggestionIndex]);
|
|
||||||
} else if (searchInput.trim()) {
|
|
||||||
setSearchTerm(searchInput);
|
|
||||||
}
|
|
||||||
setShowSuggestions(false);
|
|
||||||
setSelectedSuggestionIndex(-1);
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
setIsSearchMode(false);
|
|
||||||
setSearchInput('');
|
|
||||||
setOriginalSearchQuery('');
|
|
||||||
setSearchTerm('');
|
|
||||||
setShowSuggestions(false);
|
|
||||||
setSelectedSuggestionIndex(-1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex-1 bg-transparent focus:outline-none text-gray-700 placeholder-gray-400"
|
className="flex-1 bg-transparent focus:outline-none text-gray-700 placeholder-gray-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => handleSearch(searchInput)}
|
||||||
if (searchInput.trim()) {
|
|
||||||
setSearchTerm(searchInput);
|
|
||||||
setShowSuggestions(false);
|
|
||||||
setSelectedSuggestionIndex(-1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={searchLoading}
|
disabled={searchLoading}
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
|
@ -938,12 +813,7 @@ function Schedules() {
|
||||||
{suggestions.map((suggestion, index) => (
|
{suggestions.map((suggestion, index) => (
|
||||||
<button
|
<button
|
||||||
key={suggestion}
|
key={suggestion}
|
||||||
onClick={() => {
|
onClick={() => handleSuggestionSelect(suggestion)}
|
||||||
setSearchInput(suggestion);
|
|
||||||
setSearchTerm(suggestion);
|
|
||||||
setShowSuggestions(false);
|
|
||||||
setSelectedSuggestionIndex(-1);
|
|
||||||
}}
|
|
||||||
className={`w-full px-4 py-2 text-left text-sm flex items-center gap-3 transition-colors ${
|
className={`w-full px-4 py-2 text-left text-sm flex items-center gap-3 transition-colors ${
|
||||||
index === selectedSuggestionIndex
|
index === selectedSuggestionIndex
|
||||||
? 'bg-primary/10 text-primary'
|
? 'bg-primary/10 text-primary'
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue