import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition, } from "@headlessui/react"; import classNames from "classnames"; import { useTranslation } from "next-i18next/pages"; import { Fragment, useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { BiLogoBing } from "react-icons/bi"; import { FiSearch } from "react-icons/fi"; import { SiBaidu, SiBrave, SiDuckduckgo, SiGoogle } from "react-icons/si"; import ContainerForm from "../widget/container_form"; import Raw from "../widget/raw"; export const searchProviders = { google: { name: "Google", url: "https://www.google.com/search?q=", suggestionUrl: "https://www.google.com/complete/search?client=chrome&q=", icon: SiGoogle, }, duckduckgo: { name: "DuckDuckGo", url: "https://duckduckgo.com/?q=", suggestionUrl: "https://duckduckgo.com/ac/?type=list&q=", icon: SiDuckduckgo, }, bing: { name: "Bing", url: "https://www.bing.com/search?q=", suggestionUrl: "https://api.bing.com/osjson.aspx?query=", icon: BiLogoBing, }, baidu: { name: "Baidu", url: "https://www.baidu.com/s?wd=", suggestionUrl: "http://suggestion.baidu.com/su?&action=opensearch&ie=utf-8&wd=", icon: SiBaidu, }, brave: { name: "Brave", url: "https://search.brave.com/search?q=", suggestionUrl: "https://search.brave.com/api/suggest?&rich=false&q=", icon: SiBrave, }, custom: { name: "Custom", url: false, icon: FiSearch, }, }; function getAvailableProviderIds(provider) { if (provider && Array.isArray(provider)) { return provider.filter((value) => searchProviders.hasOwnProperty(value)); } if (provider && searchProviders[provider]) { return [provider]; } return null; } const localStorageKey = "search-name"; export function getStoredProvider() { if (typeof window !== "undefined") { const storedName = localStorage.getItem(localStorageKey); if (storedName) { return Object.values(searchProviders).find((el) => el.name === storedName); } } return null; } function subscribeToStoredProvider(onStoreChange) { const handleStorage = (event) => { if (event.key === localStorageKey) onStoreChange(); }; window.addEventListener("storage", handleStorage); return () => window.removeEventListener("storage", handleStorage); } export default function Search({ options }) { const { t } = useTranslation(); // options is a fresh object each render, so memo on provider itself const availableProviderIds = useMemo(() => getAvailableProviderIds(options.provider) ?? [], [options.provider]); const storedProvider = useSyncExternalStore(subscribeToStoredProvider, getStoredProvider, () => null); const storedProviderId = Object.keys(searchProviders).find( (providerId) => searchProviders[providerId] === storedProvider, ); const initialProvider = availableProviderIds.includes(storedProviderId) ? storedProvider : searchProviders[availableProviderIds[0] ?? "google"]; const [query, setQuery] = useState(""); const [providerOverride, setProviderOverride] = useState(null); const [searchSuggestions, setSearchSuggestions] = useState([]); const selectedProvider = providerOverride ?? initialProvider; useEffect(() => { const abortController = new AbortController(); if ( options.showSearchSuggestions && (selectedProvider.suggestionUrl || options.suggestionUrl) && // custom providers pass url via options query.trim().length > 0 && 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(); if (newSearchSuggestions) { if (newSearchSuggestions[1].length > 4) { newSearchSuggestions[1] = newSearchSuggestions[1].splice(0, 4); } setSearchSuggestions(newSearchSuggestions); } }) .catch(() => { // If there is an error, just ignore it. There just will be no search suggestions. }); } return () => { abortController.abort(); }; }, [selectedProvider, options, query, searchSuggestions]); function doSearch(value) { const q = encodeURIComponent(value); const { url } = selectedProvider; if (url) { window.open(`${url}${q}`, options.target || "_blank"); } else { window.open(`${options.url}${q}`, options.target || "_blank"); } setQuery(""); } const handleSearchKeyDown = (event) => { if (event.key === "Enter" && !searchSuggestions[1]?.length) { doSearch(event.target.value); } }; if (!availableProviderIds.length) { return null; } const onChangeProvider = (provider) => { setProviderOverride(provider); localStorage.setItem(localStorageKey, provider.name); }; return (
{ setQuery(event.target.value); }} required autoCapitalize="off" autoCorrect="off" autoComplete="off" autoFocus={options.focus} onBlur={(e) => e.preventDefault()} onKeyDown={handleSearchKeyDown} />
{availableProviderIds.map((providerId) => { const p = searchProviders[providerId]; return ( {({ active }) => (
  • )}
    ); })}
    {searchSuggestions[1]?.length > 0 && (
    {query} {searchSuggestions[1].map((suggestion) => ( {({ active }) => (
    {suggestion.indexOf(query) === 0 ? query : ""} {suggestion.indexOf(query) === 0 ? suggestion.substring(query.length) : suggestion}
    )}
    ))}
    )}
    ); }