From d5af7eda639a0bb65ccddd22050818c15f2445c3 Mon Sep 17 00:00:00 2001 From: Florian Hye <31217036+Flo2410@users.noreply.github.com> Date: Thu, 1 Feb 2024 02:17:42 +0100 Subject: [PATCH] Feature: search suggestions for search and quick launch (#2775) --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/configs/settings.md | 2 + docs/widgets/info/search.md | 26 ++- public/locales/de/common.json | 3 +- public/locales/en/common.json | 3 +- src/components/quicklaunch.jsx | 78 +++++++- src/components/widgets/search/search.jsx | 220 +++++++++++++++-------- src/pages/api/search/searchSuggestion.js | 23 +++ src/pages/index.jsx | 6 +- 8 files changed, 269 insertions(+), 92 deletions(-) create mode 100644 src/pages/api/search/searchSuggestion.js diff --git a/docs/configs/settings.md b/docs/configs/settings.md index fdc5eff2..d3e9a837 100644 --- a/docs/configs/settings.md +++ b/docs/configs/settings.md @@ -359,12 +359,14 @@ There are a few optional settings for the Quick Launch feature: - `searchDescriptions`: which lets you control whether item descriptions are included in searches. This is off by default. When enabled, results that match the item name will be placed above those that only match the description. - `hideInternetSearch`: disable automatically including the currently-selected web search (e.g. from the widget) as a Quick Launch option. This is false by default, enabling the feature. +- `showSearchSuggestions`: shows search suggestions for the internet search. This value will be inherited from the search widget if it is not specified. If it is not specified there either, it will default to false. - `hideVisitURL`: disable detecting and offering an option to open URLs. This is false by default, enabling the feature. ```yaml quicklaunch: searchDescriptions: true hideInternetSearch: true + showSearchSuggestions: true hideVisitURL: true ``` diff --git a/docs/widgets/info/search.md b/docs/widgets/info/search.md index a9851bb1..faae6c37 100644 --- a/docs/widgets/info/search.md +++ b/docs/widgets/info/search.md @@ -9,6 +9,7 @@ You can add a search bar to your top widget area that can search using Google, D - search: provider: google # google, duckduckgo, bing, baidu, brave or custom focus: true # Optional, will set focus to the search bar on page load + showSearchSuggestions: true # Optional, will show search suggestions. Defaults to false target: _blank # One of _self, _blank, _parent or _top ``` @@ -17,8 +18,10 @@ or for a custom search: ```yaml - search: provider: custom - url: https://lougle.com/?q= + url: https://www.ecosia.org/search?q= target: _blank + suggestionUrl: https://ac.ecosia.org/autocomplete?type=list&q= # Optional + showSearchSuggestions: true # Optional ``` multiple providers is also supported via a dropdown (excluding custom): @@ -28,4 +31,25 @@ multiple providers is also supported via a dropdown (excluding custom): provider: [brave, google, duckduckgo] ``` +The response body for the URL provided with the `suggestionUrl` option should look like this: + +```json +[ + "home", + [ + "home depot", + "home depot near me", + "home equity loan", + "homeworkify", + "homedepot.com", + "homebase login", + "home depot credit card", + "home goods" + ] +] +``` + +The first entry of the array contains the search query, the second one is an array of the suggestions. +In the example above, the search query was **home**. + _Added in v0.1.6, updated in 0.6.0_ diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 4d2a5b17..93d41110 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -419,7 +419,8 @@ "search": "Suchen", "custom": "Benutzerdefiniert", "visit": "Besuchen", - "url": "URL" + "url": "URL", + "searchsuggestion": "Vorschlag" }, "wmo": { "0-day": "sonnig", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 596377d5..5521fd0c 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -419,7 +419,8 @@ "search": "Search", "custom": "Custom", "visit": "Visit", - "url": "URL" + "url": "URL", + "searchsuggestion": "Suggestion" }, "wmo": { "0-day": "Sunny", diff --git a/src/components/quicklaunch.jsx b/src/components/quicklaunch.jsx index 7fb1460a..80d61ee0 100644 --- a/src/components/quicklaunch.jsx +++ b/src/components/quicklaunch.jsx @@ -15,16 +15,19 @@ export default function QuickLaunch({ searchProvider, }) { const { t } = useTranslation(); + const { settings } = useContext(SettingsContext); - const { searchDescriptions, hideVisitURL } = settings?.quicklaunch - ? settings.quicklaunch - : { searchDescriptions: false, hideVisitURL: false }; + const { searchDescriptions = false, hideVisitURL = false } = settings?.quicklaunch ?? {}; + const showSearchSuggestions = !!( + settings?.quicklaunch?.showSearchSuggestions ?? searchProvider.showSearchSuggestions + ); const searchField = useRef(); const [results, setResults] = useState([]); const [currentItemIndex, setCurrentItemIndex] = useState(null); const [url, setUrl] = useState(null); + const [searchSuggestions, setSearchSuggestions] = useState([]); function openCurrentItem(newWindow) { const result = results[currentItemIndex]; @@ -36,8 +39,9 @@ export default function QuickLaunch({ setTimeout(() => { setSearchString(""); setCurrentItemIndex(null); + setSearchSuggestions([]); }, 200); // delay a little for animations - }, [close, setSearchString, setCurrentItemIndex]); + }, [close, setSearchString, setCurrentItemIndex, setSearchSuggestions]); function handleSearchChange(event) { const rawSearchString = event.target.value.toLowerCase(); @@ -90,6 +94,8 @@ export default function QuickLaunch({ } useEffect(() => { + const abortController = new AbortController(); + if (searchString.length === 0) setResults([]); else { let newResults = servicesAndBookmarks.filter((r) => { @@ -109,9 +115,43 @@ export default function QuickLaunch({ if (searchProvider) { newResults.push({ href: searchProvider.url + encodeURIComponent(searchString), - name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")} `, + name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")}`, type: "search", }); + + if (showSearchSuggestions && searchProvider.suggestionUrl) { + if (searchString.trim() !== searchSuggestions[0]) { + fetch( + `/api/search/searchSuggestion?query=${encodeURIComponent(searchString)}&providerName=${ + searchProvider.name ?? "Custom" + }`, + { 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. + }); + } + + if (searchSuggestions[1]) { + newResults = newResults.concat( + searchSuggestions[1].map((suggestion) => ({ + href: searchProvider.url + encodeURIComponent(suggestion), + name: suggestion, + type: "searchSuggestion", + })), + ); + } + } } if (!hideVisitURL && url) { @@ -128,7 +168,21 @@ export default function QuickLaunch({ setCurrentItemIndex(0); } } - }, [searchString, servicesAndBookmarks, searchDescriptions, hideVisitURL, searchProvider, url, t]); + + return () => { + abortController.abort(); + }; + }, [ + searchString, + servicesAndBookmarks, + searchDescriptions, + hideVisitURL, + showSearchSuggestions, + searchSuggestions, + searchProvider, + url, + t, + ]); const [hidden, setHidden] = useState(true); useEffect(() => { @@ -219,7 +273,17 @@ export default function QuickLaunch({ )}