import { type JSX, useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/router'; import Downshift, { type DownshiftState, type StateChangeOptions, } from 'downshift'; import { useAtomValue } from 'jotai'; import { Modal, ModalBody } from 'reactstrap'; import { useCurrentPagePath } from '~/states/page'; import { useSetSearchKeyword } from '~/states/search'; import { isSearchScopeChildrenAsDefaultAtom } from '~/states/server-configurations'; import { isIncludeAiMenthion, removeAiMenthion } from '../../utils/ai'; import type { DownshiftItem } from '../interfaces/downshift'; import { useSearchModalActions, useSearchModalStatus, } from '../states/modal/search'; import { SearchForm } from './SearchForm'; import { SearchHelp } from './SearchHelp'; import { SearchMethodMenuItem } from './SearchMethodMenuItem'; import { SearchResultMenuItem } from './SearchResultMenuItem'; type Props = { onSearch: (keyword: string) => void; }; const SearchModalSubstance = (props: Props): JSX.Element => { const { onSearch } = props; const [searchInput, setSearchInput] = useState(''); const [isMenthionedToAi, setMenthionedToAi] = useState(false); const searchModalData = useSearchModalStatus(); const { close: closeSearchModal } = useSearchModalActions(); const router = useRouter(); const changeSearchTextHandler = useCallback((searchText: string) => { setSearchInput(searchText); }, []); const selectSearchMenuItemHandler = useCallback( async (selectedItem: DownshiftItem) => { await router.push(selectedItem.url); closeSearchModal(); }, [closeSearchModal, router], ); const submitHandler = useCallback(() => { onSearch(searchInput); closeSearchModal(); }, [closeSearchModal, onSearch, searchInput]); // Memoize stateReducer to prevent recreation on every render const stateReducer = useCallback( ( state: DownshiftState, changes: StateChangeOptions, ) => { // Do not update highlightedIndex on mouse hover if (changes.type === Downshift.stateChangeTypes.itemMouseEnter) { return { ...changes, highlightedIndex: state.highlightedIndex, }; } return changes; }, [], ); useEffect(() => { if (!searchModalData?.isOpened) { return; } if (searchModalData?.searchKeyword == null) { setSearchInput(''); } else { setSearchInput(searchModalData.searchKeyword); } }, [searchModalData?.isOpened, searchModalData?.searchKeyword]); useEffect(() => { setMenthionedToAi(isIncludeAiMenthion(searchInput)); }, [searchInput]); // Memoize AI mention removal to prevent recalculation on every render const searchInputWithoutAi = useMemo( () => removeAiMenthion(searchInput), [searchInput], ); // Memoize icon selection to prevent recalculation const searchIcon = useMemo( () => (isMenthionedToAi ? 'psychology' : 'search'), [isMenthionedToAi], ); // Memoize icon class to prevent string concatenation on every render const iconClassName = useMemo( () => `material-symbols-outlined fs-4 me-3 ${isMenthionedToAi ? 'text-primary' : ''}`, [isMenthionedToAi], ); return ( {({ getRootProps, getInputProps, getItemProps, getMenuProps, highlightedIndex, }) => (
{searchIcon}
)}
); }; const SearchModal = (): JSX.Element => { const { isOpened, onSearchOverride } = useSearchModalStatus(); const { close: closeSearchModal } = useSearchModalActions(); const setSearchKeyword = useSetSearchKeyword(); const isSearchScopeChildrenAsDefault = useAtomValue( isSearchScopeChildrenAsDefaultAtom, ); const currentPagePath = useCurrentPagePath(); const searchHandler = useCallback( (keyword: string) => { // invoke override function if exists if (onSearchOverride != null) { onSearchOverride(keyword); return; } // Respect the admin setting: scope search to the current page tree by default const shouldScopeToChildren = isSearchScopeChildrenAsDefault && currentPagePath != null; const finalKeyword = shouldScopeToChildren ? `prefix:${currentPagePath} ${keyword}` : keyword; setSearchKeyword(finalKeyword); }, [ onSearchOverride, setSearchKeyword, isSearchScopeChildrenAsDefault, currentPagePath, ], ); return ( {isOpened && } ); }; export default SearchModal;