From a9cf5630feec6cc321542a9b74a24caee6e98df5 Mon Sep 17 00:00:00 2001 From: Alikia2x Date: Wed, 17 Apr 2024 23:44:28 +0800 Subject: [PATCH] feature: complete function and code logic for suggestion (include cloud) todo: problem: In a short period of time, several functions that depend on the latest state at the same time do operations and update the state according to the state they got, which will cause all calls except the first one to update the state incorrectly due to incorrect dependencies (not-updated state) because of the asynchronous updating of the React state. --- components/search/onesearch/onesearch.tsx | 115 +++++++++++++++++++- components/search/onesearch/plainSearch.tsx | 18 +++ components/state/settings.ts | 53 +++++---- global.d.ts | 25 +++-- lib/search.ts | 4 + 5 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 components/search/onesearch/plainSearch.tsx create mode 100644 lib/search.ts diff --git a/components/search/onesearch/onesearch.tsx b/components/search/onesearch/onesearch.tsx index 402f875..8a24671 100644 --- a/components/search/onesearch/onesearch.tsx +++ b/components/search/onesearch/onesearch.tsx @@ -1,16 +1,19 @@ import { useEffect, useState } from "react"; import SuggestionBox from "./suggestionBox"; -import Suggestion from "./suggestion"; import { useRecoilValue } from "recoil"; import { queryState } from "@/components/state/query"; -import { Suggestion as SuggestionType } from "search-engine-autocomplete"; import { useLocale, useTranslations } from "next-intl"; import { suggestionItem, suggestionsResponse } from "@/global"; import getSearchEngineName from "@/lib/getSearchEngineName"; +import PlainSearch from "./plainSearch"; export default function () { - const [suggestion, setSuggetsion] = useState([] as suggestionItem[]); + const [suggestion, setFinalSuggetsion] = useState([] as suggestionItem[]); + const [selected, setSelected] = useState(0); const [lastUpdate, setLastUpdate] = useState(0); + const [cloudLastUpdate, setCloudLastUpdate] = useState(0); + const [clientLastUpdate, setClientLastUpdate] = useState(0); + const devMode = true; const query = useRecoilValue(queryState); const engineName = getSearchEngineName(); const lang = useLocale(); @@ -18,18 +21,118 @@ export default function () { useEffect(() => { const time = new Date().getTime().toString(); + if (query.trim() === "") { + cleanSuggestion("QUERY", new Date().getTime()); + return; + } fetch(`/api/suggestion?q=${query}&l=${lang}&t=${time}`) .then((res) => res.json()) .then((data: suggestionsResponse) => { + let suggestionToUpdate: suggestionItem[] = data.suggestions; + if (data.verbatimRelevance) { + suggestionToUpdate.push({ + type: "default", + suggestion: query, + relevance: data.verbatimRelevance + }); + } + updateSuggestion(suggestionToUpdate, "cloud", data.time); }); - }, [query, engineName]); + }, [query]); + + function setSuggestion(data: suggestionItem[], source: "frontend" | "client" | "cloud", time?: number) { + let finalSuggestion = data.sort((a, b) => b.relevance - a.relevance); + finalSuggestion = finalSuggestion.filter( + (item, index, self) => index === self.findIndex((t) => t.suggestion === item.suggestion) + ); + setFinalSuggetsion(finalSuggestion); + const requestUpdateTime = time || new Date().getTime(); + const overwriteLastUpdate = requestUpdateTime > lastUpdate; + switch (source) { + case "frontend": + setLastUpdate(new Date().getTime()); + break; + case "client": + setClientLastUpdate(requestUpdateTime); + if (overwriteLastUpdate) setLastUpdate(new Date().getTime()); + case "cloud": + setCloudLastUpdate(requestUpdateTime); + if (overwriteLastUpdate) setLastUpdate(new Date().getTime()); + break; + default: + break; + } + } + + function updateSuggestion(data: suggestionItem[], source: "frontend" | "client" | "cloud", time?: number) { + let finalSuggestion = suggestion; + const types: string[] = []; + for (let sug of data) { + if (!types.includes(sug.type)) types.push(sug.type); + } + for (let type of types) { + finalSuggestion = finalSuggestion.filter((item) => { + return item.type !== type; + }); + } + finalSuggestion = finalSuggestion.concat(data); + setSuggestion(finalSuggestion, source, time); + } + + function cleanSuggestion(type: string, time?: number){ + let finalSuggestion = suggestion; + console.log("1",finalSuggestion); + finalSuggestion = finalSuggestion.filter((item) => { + return item.type !== type; + }); + console.log("2",finalSuggestion); + setSuggestion(finalSuggestion, "frontend", time); + } useEffect(() => { - setLastUpdate(new Date().getTime()); - }, [query]); + updateSuggestion( + [ + { + type: "default", + suggestion: query, + relevance: 2000 + } + ], + "frontend" + ); + }, [query, engineName]); return ( + {suggestion.map((s) => { + if (s.suggestion.trim() === "") return; + if (s.type === "default") { + return ( + + {s.suggestion}  + + {t("search-help-text", { engine: engineName })} + + {devMode && ( + +   Relevance: {s.relevance} + + )} + + ); + } else if (s.type === "QUERY") { + return ( + + {s.suggestion} + {devMode && ( + +   Relevance: {s.relevance} + + )} + + ); + } + })} ); } diff --git a/components/search/onesearch/plainSearch.tsx b/components/search/onesearch/plainSearch.tsx new file mode 100644 index 0000000..b545a43 --- /dev/null +++ b/components/search/onesearch/plainSearch.tsx @@ -0,0 +1,18 @@ +import search from "@/lib/search"; +import { settingsState } from "@/components/state/settings"; +import { useRecoilValue } from "recoil"; + +export default function (props: { children: React.ReactNode, query: string }) { + const settings = useRecoilValue(settingsState); + const engine = settings.searchEngines[settings.currentSearchEngine]; + const newTab = settings.searchInNewTab; + return ( +
{search(props.query, engine, newTab)}} + > + {props.children} +
+ ); +} diff --git a/components/state/settings.ts b/components/state/settings.ts index 44e7dfd..3eb45ab 100644 --- a/components/state/settings.ts +++ b/components/state/settings.ts @@ -1,35 +1,50 @@ +import { settingsType } from "@/global"; import isLocalStorageAvailable from "@/lib/isLocalStorageAvailable"; import { atom } from "recoil"; -const defaultSettings: settings = { - version: 1, - elementBackdrop: true, - bgBlur: true, - timeShowSecond: false, - currentSearchEngine: "google", - searchEngines: { - google: "https://www.google.com/search?q=%s", - bing: "https://www.bing.com/search?q=%s", - baidu: "https://www.baidu.com/s?wd=%s", - duckduckgo: "https://duckduckgo.com/?q=%s", - yandex: "https://yandex.com/search/?text=%s", - yahoo: "https://search.yahoo.com/search?p=%s", - ecosia: "https://www.ecosia.org/search?q=%s" +const defaultSettings: settingsType = { + "version": 2, + "elementBackdrop": true, + "bgBlur": true, + "timeShowSecond": false, + "currentSearchEngine": "google", + "searchInNewTab": true, + "searchEngines": { + "google": "https://www.google.com/search?q=%s", + "bing": "https://www.bing.com/search?q=%s", + "baidu": "https://www.baidu.com/s?wd=%s", + "duckduckgo": "https://duckduckgo.com/?q=%s", + "yandex": "https://yandex.com/search/?text=%s", + "yahoo": "https://search.yahoo.com/search?p=%s", + "ecosia": "https://www.ecosia.org/search?q=%s" } }; + const localStorageEffect = - (key: any) => + (key: string) => ({ setSelf, onSet }: any) => { if (isLocalStorageAvailable()===false){ return; } - const savedValue = localStorage.getItem(key); - if (savedValue != null) { - setSelf(JSON.parse(savedValue)); + if (localStorage.getItem(key) === null) { + localStorage.setItem(key, JSON.stringify(defaultSettings)); + return; } + let settings =JSON.parse(JSON.stringify(defaultSettings)); + const savedSettings = localStorage.getItem(key)!; + const parsedSettings = JSON.parse(savedSettings); + + Object.keys(settings).map((key) => { + if (parsedSettings[key] !== undefined && key !== "version"){ + settings[key] = parsedSettings[key]; + } + }) - onSet((newValue: settings) => { + setSelf(settings); + localStorage.setItem(key, JSON.stringify(settings)); + + onSet((newValue: settingsType) => { localStorage.setItem(key, JSON.stringify(newValue)); }); }; diff --git a/global.d.ts b/global.d.ts index 5718700..d575df9 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,17 +1,18 @@ import { Suggestion } from "search-engine-autocomplete"; -type settingsType = { - version: number; - elementBackdrop: boolean; - bgBlur: boolean; - timeShowSecond: boolean; - currentSearchEngine: string; - searchEngines: { - [key: string]: string, - }; +interface settingsType extends Object{ + "version": number, + "elementBackdrop": boolean, + "bgBlur": boolean, + "timeShowSecond": boolean, + "currentSearchEngine": string, + "searchInNewTab": boolean, + "searchEngines": { + [key: string]: string + }, }; -type suggestionsResponse = { +interface suggestionsResponse extends Object{ suggestions: Suggestion[], query: string, verbatimRelevance: number, @@ -21,6 +22,6 @@ type suggestionsResponse = { type suggestionItem = { suggestion: string, type: string, - relativeRelevance: number, - relevance?: number + relativeRelevance?: number, + relevance: number } \ No newline at end of file diff --git a/lib/search.ts b/lib/search.ts new file mode 100644 index 0000000..fee5d50 --- /dev/null +++ b/lib/search.ts @@ -0,0 +1,4 @@ +export default function(query: string, engine: string, newTab: boolean = true) { + if(newTab) window.open(engine.replace("%s", query)); + else window.location.href = engine.replace("%s", query); +} \ No newline at end of file