diff --git a/bun.lockb b/bun.lockb index 66f3594..caaa0da 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/engineSelector.tsx b/components/engineSelector.tsx index b2c3464..7959fac 100644 --- a/components/engineSelector.tsx +++ b/components/engineSelector.tsx @@ -1,21 +1,22 @@ -import React, { useEffect, useState } from "react"; -import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "@nextui-org/react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { settingsAtom } from "lib/state/settings"; import { engineTranslation } from "lib/onesearch/translatedEngineList"; import { settingsType } from "global"; import { useAtomValue, useSetAtom } from "jotai"; +import Picker, { PickedItem } from "./picker"; export default function EngineSelector(props: { className: string }) { const { t } = useTranslation(); const settings: settingsType = useAtomValue(settingsAtom); - const items = settings.searchEngines; + const engines = settings.searchEngines; const currentEngine: string = settings.currentSearchEngine; - const displayEngine = getName(currentEngine); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [selectedKeys, setSelectedKeys] = useState(new Set([currentEngine]) as any); - const selectedValue = React.useMemo(() => Array.from(selectedKeys).join(", "), [selectedKeys]); + const [selected, setSelected] = useState(currentEngine); const setSettings = useSetAtom(settingsAtom); + let engineList: PickedItem = {}; + for (const engineKey of Object.keys(engines)) { + engineList[engineKey] = getName(engineKey); + } function getName(engineKey: string) { return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey; @@ -30,34 +31,20 @@ export default function EngineSelector(props: { className: string }) { }; }); } - if (selectedValue !== currentEngine) { - setEngine(selectedValue); + if (selected !== currentEngine) { + setEngine(selected); } - }, [currentEngine, selectedValue, setSettings]); + }, [currentEngine, selected, setSettings]); return ( -
- - - - - - {Object.keys(items).map((item) => ( - - {getName(item)} - - ))} - - -
+ { + setSelected(selected); + }} + displayContent={getName(selected)} + className={props.className} + /> ); } diff --git a/components/onesearch/onesearch.tsx b/components/onesearch/onesearch.tsx index 7f686f7..73aaaab 100644 --- a/components/onesearch/onesearch.tsx +++ b/components/onesearch/onesearch.tsx @@ -23,11 +23,11 @@ export default function OneSearch() { const lastRequestTimeRef = useRef(0); const selected = useAtomValue(selectedSuggestionAtom); const settings = useAtomValue(settingsAtom); - const devMode = true; + const devMode = false; const query = useAtomValue(queryAtom); const engineName = getSearchEngineName(); const engine = settings.currentSearchEngine; - const { t } = useTranslation("Search"); + const { t } = useTranslation(); const lang = i18next.language; useEffect(() => { @@ -127,7 +127,7 @@ export default function OneSearch() { {s.suggestion}  - {t("search-help-text", { engine: engineName })} + {t("search.search-help-text", { engine: engineName })} {devMode && ( diff --git a/components/onesearch/suggestionBox.tsx b/components/onesearch/suggestionBox.tsx index 70637a5..e7cefb3 100644 --- a/components/onesearch/suggestionBox.tsx +++ b/components/onesearch/suggestionBox.tsx @@ -1,8 +1,10 @@ export default function SuggestionBox(props: { children?: React.ReactNode }) { return ( -
+
{props.children}
); diff --git a/components/picker.tsx b/components/picker.tsx new file mode 100644 index 0000000..4b7e2dc --- /dev/null +++ b/components/picker.tsx @@ -0,0 +1,148 @@ +import { HTMLAttributes, RefObject, useEffect, useRef, useState } from "react"; +import { selectedOnChange } from "./selectorItem"; +import { createPortal } from "react-dom"; +import { Icon } from "@iconify/react"; +import React from "react"; + +export type selectionType = string; + +interface PickerProps extends HTMLAttributes { + selected: selectionType; + selectionOnChange: selectedOnChange; + displayContent: string; + selectionItems: PickedItem; +} + +export interface PickedItem { + [key: string]: selectionType; +} + +export default function Picker(props: PickerProps) { + const itemListRef: RefObject = useRef(null); + const buttonRef: RefObject = useRef(null); + const [displayList, setDisplayList] = useState(false); + + const updatePosition = () => { + if (itemListRef.current == null || buttonRef.current == null) { + return; + } + const buttonRect = buttonRef.current.getBoundingClientRect(); + const listWidth = itemListRef.current.getBoundingClientRect().width; + // Align to center + itemListRef.current.style.left = buttonRect.x + buttonRect.width / 2 - listWidth / 2 + "px"; + itemListRef.current.style.top = buttonRect.y + buttonRect.height + 16 + "px"; + }; + + useEffect(() => { + updatePosition(); + const handleResize = () => { + updatePosition(); + }; + + window.addEventListener("resize", handleResize); + + // Cleanup event listener on component unmount + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [itemListRef, buttonRef]); + + function toggleDisplay(targetState?: boolean) { + function hideList() { + if (itemListRef.current) { + itemListRef.current.style.opacity = "0%"; + itemListRef.current.style.transform = "scaleX(.85) scaleY(.85)"; + } + setTimeout(() => { + setDisplayList(false); + }, 200); + } + function showList() { + setDisplayList(true); + setTimeout(() => { + updatePosition(); + if (itemListRef.current) { + itemListRef.current.style.opacity = "100%"; + itemListRef.current.style.transform = "scaleX(1) scaleY(1)"; + } + }, 20); + } + if (targetState === true) { + showList(); + } else if (targetState === false) { + hideList(); + } else if (displayList === true) { + hideList(); + } else { + showList(); + } + } + + const { displayContent, selectionOnChange, selectionItems, selected, ...rest } = props; + return ( +
+ + {displayList && ( + + )} +
+ ); +} + +interface PickerListProps { + selected: selectionType; + selectionOnChange: selectedOnChange; + selectionItems: PickedItem; + toggleDisplay: Function; +} + +const PickerList = React.forwardRef((props, ref) => { + const { selected, selectionOnChange, selectionItems } = props; + + return createPortal( +
+ {Object.keys(selectionItems).map((key: string, index) => { + return ( +
{ + selectionOnChange(key); + props.toggleDisplay(); + }} + > + {selectionItems[key]} +
+ {key === selected && ( + + )} +
+ ); + })} +
, + document.body + ); +}); + +PickerList.displayName = "PickerList"; diff --git a/components/selector.tsx b/components/selector.tsx deleted file mode 100644 index ddc7f5d..0000000 --- a/components/selector.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { HTMLAttributes, ReactNode, RefObject, useEffect, useRef } from "react"; -import { selectedOnChange } from "./selectorItem"; -import { createPortal } from "react-dom"; - -export type selectionType = string | number | boolean; - -interface PickerPropsChildrenStyle extends HTMLAttributes { - selected: selectionType; - selectionOnChange: selectedOnChange; - displayContent: string; - children: ReactNode; -} - -interface PickerPropsParamStyle extends HTMLAttributes { - selected: selectionType; - selectionOnChange: selectedOnChange; - displayContent: string; - selectionItems: PickedItem; -} - -interface PickedItem { - [key: string]: selectionType; -} - -export default function Picker(props: PickerPropsChildrenStyle | PickerPropsParamStyle) { - const itemListRef: RefObject = useRef(null); - const buttonRef: RefObject = useRef(null); - - const updatePosition = () => { - if (itemListRef.current == null || buttonRef.current == null) { - return; - } - const buttonRect = buttonRef.current.getBoundingClientRect(); - const listWidth = itemListRef.current.getBoundingClientRect().width; - // Align to center - itemListRef.current.style.left = buttonRect.x + buttonRect.width / 2 - listWidth / 2 + "px"; - itemListRef.current.style.top = buttonRect.y + buttonRect.height + 16 + "px"; - }; - - useEffect(() => { - updatePosition(); - const handleResize = () => { - updatePosition(); - }; - - window.addEventListener('resize', handleResize); - - // Cleanup event listener on component unmount - return () => { - window.removeEventListener('resize', handleResize); - }; - }, [itemListRef, buttonRef]); - - if ("selectionItems" in props) { - const { selectionItems, displayContent, selectionOnChange, ...rest } = props; - return ( -
- - {createPortal( -
- {Object.keys(selectionItems).map((key: string, index) => { - return
{selectionItems[key]}
; - })} -
, - document.body - )} -
- ); - } else { - return ( -
- - {createPortal(
{props.children}
, document.body)} -
- ); - } -} diff --git a/components/selectorItem.tsx b/components/selectorItem.tsx index 94469fa..1365af0 100644 --- a/components/selectorItem.tsx +++ b/components/selectorItem.tsx @@ -1,5 +1,5 @@ import { ReactNode } from "react"; -import { selectionType } from "./selector"; +import { selectionType } from "./picker"; export type selectedOnChange = (target: selectionType) => void; diff --git a/lib/onesearch/getSearchEngineName.ts b/lib/onesearch/getSearchEngineName.ts index 4d6eac6..251a151 100644 --- a/lib/onesearch/getSearchEngineName.ts +++ b/lib/onesearch/getSearchEngineName.ts @@ -12,6 +12,6 @@ export default function(){ } function getName(engineKey: string) { - const { t } = useTranslation("Search"); - return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey; + const { t } = useTranslation(); + return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey; } \ No newline at end of file diff --git a/package.json b/package.json index 4acdd13..25458a5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "NODE_ENV=production bun server.ts" }, "dependencies": { + "@iconify/react": "^5.0.1", "@nextui-org/react": "^2.4.2", "@types/express": "^4.17.21", "cac": "^6.7.14", diff --git a/pages/index.tsx b/pages/index.tsx index 9219879..4fca8f9 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -6,7 +6,6 @@ import { settingsAtom } from "lib/state/settings"; import { bgFocusAtom } from "lib/state/background"; import EngineSelector from "components/engineSelector"; import OneSearch from "components/onesearch/onesearch"; -import Picker from "components/selector"; export default function Homepage() { const settings = useAtomValue(settingsAtom); @@ -16,20 +15,13 @@ export default function Homepage() {
setBgFocus(true)} />
); } diff --git a/server.ts b/server.ts index 46e9c34..b43687f 100644 --- a/server.ts +++ b/server.ts @@ -4,6 +4,7 @@ import ViteExpress from "vite-express"; import pjson from "./package.json"; import { networkInterfaces } from "os"; import cac from "cac"; +import { completeGoogle } from "search-engine-autocomplete"; const start = new Date(); const cli = cac(); @@ -37,6 +38,21 @@ if (parsed.options.host!==undefined && typeof parsed.options.host == "boolean" & app.get("/message", (_, res) => res.send("Hello from express!")); +app.get('/api/suggestion', async (req, res) => { + const query = req.query.q as string; + const t = parseInt(req.query.t as string || "0") || null; + let language = req.query.l as string || 'en-US'; + + try { + const data = await completeGoogle(query, language); + //logger.info({ type: "onesearch_search_autocomplete", query: query, data: data }); + res.json({ ...data, time: t }); + } catch (error) { + //logger.error({ type: "onesearch_search_autocomplete_error", error: error.message }); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + async function helloMessage() { const { base } = await ViteExpress.getViteConfig(); //console.clear();