diff --git a/src/components/widgets/search/search.jsx b/src/components/widgets/search/search.jsx index 4a017d3f..414f38fc 100644 --- a/src/components/widgets/search/search.jsx +++ b/src/components/widgets/search/search.jsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, Fragment } from "react"; import { useTranslation } from "next-i18next"; import { FiSearch } from "react-icons/fi"; import { SiDuckduckgo, SiMicrosoftbing, SiGoogle, SiBaidu, SiBrave } from "react-icons/si"; -import { Listbox, Transition } from "@headlessui/react"; +import { Listbox, Transition, Combobox } from "@headlessui/react"; import classNames from "classnames"; import ContainerForm from "../widget/container_form"; @@ -77,6 +77,7 @@ export default function Search({ options }) { const [selectedProvider, setSelectedProvider] = useState( searchProviders[availableProviderIds[0] ?? searchProviders.google], ); + const [searchSuggestions, setSearchSuggestions] = useState([]); useEffect(() => { const storedProvider = getStoredProvider(); @@ -87,9 +88,40 @@ export default function Search({ options }) { } }, [availableProviderIds]); + useEffect(() => { + const abortController = new AbortController(); + + if (options.showSearchSuggestions && selectedProvider.suggestionUrl && query.trim() !== searchSuggestions[0]) { + fetch(`/api/search/searchSuggestion?query=${encodeURIComponent(query)}&providerName=${selectedProvider.name}`, { + signal: abortController.signal, + }) + .then(async (searchSuggestionResult) => { + const newSearchSuggestions = await searchSuggestionResult.json(); + + // Check if there is a search suggestion + if (newSearchSuggestions) { + // Restrict the searchSuggestion to 4 entries + if (newSearchSuggestions[1].length > 4) { + newSearchSuggestions[1] = newSearchSuggestions[1].splice(0, 4); + } + + // Save the new search suggestions in their state. + setSearchSuggestions(newSearchSuggestions); + } + }) + .catch(() => { + // If there is an error, just ignore it. There just will be no search suggestions. + }); + } + + return () => { + abortController.abort(); + }; + }, [selectedProvider, options.showSearchSuggestions, query, searchSuggestions]); + const submitCallback = useCallback( - (event) => { - const q = encodeURIComponent(query); + (value) => { + const q = encodeURIComponent(value); const { url } = selectedProvider; if (url) { window.open(`${url}${q}`, options.target || "_blank"); @@ -97,11 +129,9 @@ export default function Search({ options }) { window.open(`${options.url}${q}`, options.target || "_blank"); } - event.preventDefault(); - event.target.reset(); setQuery(""); }, - [options.target, options.url, query, selectedProvider], + [selectedProvider, options.url, options.target], ); if (!availableProviderIds) { @@ -114,84 +144,110 @@ export default function Search({ options }) { }; return ( - +
- setQuery(s.currentTarget.value)} - required - autoCapitalize="off" - autoCorrect="off" - autoComplete="off" - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus={options.focus} - /> - -
- - - {t("search.search")} - -
- + setQuery(event.target.value)} + required + autoCapitalize="off" + autoCorrect="off" + autoComplete="off" + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={options.focus} + /> + + - + + + {t("search.search")} + +
+ -
- {availableProviderIds.map((providerId) => { - const p = searchProviders[providerId]; - return ( - - {({ active }) => ( -
  • - -
  • - )} -
    - ); - })} + +
    + {availableProviderIds.map((providerId) => { + const p = searchProviders[providerId]; + return ( + + {({ active }) => ( +
  • + +
  • + )} +
    + ); + })} +
    +
    + + + + {searchSuggestions[1]?.length > 0 && ( + +
    + + + {searchSuggestions[1].map((suggestion) => ( + + {({ active }) => ( + + {suggestion} + + )} + + ))}
    - - - +
    + )} +