diff --git a/src/components/account.tsx b/src/components/account.tsx index 62facae..399f31e 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -1,7 +1,6 @@ import { Client, CredentialManager } from "@atcute/client"; import { Did } from "@atcute/lexicons"; import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; -import { A } from "@solidjs/router"; import { createSignal, For, onMount, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { resolveDidDoc } from "../utils/api.js"; @@ -76,31 +75,24 @@ const AccountManager = () => {
{(did) => ( -
- -
- setOpenManager(false)} - class="flex items-center p-1" - > - - +
+
+
)} diff --git a/src/components/search.tsx b/src/components/search.tsx index c09395a..d7f1667 100644 --- a/src/components/search.tsx +++ b/src/components/search.tsx @@ -1,6 +1,8 @@ -import { useLocation, useNavigate } from "@solidjs/router"; -import { createSignal, onCleanup, onMount, Show } from "solid-js"; +import { Client, CredentialManager } from "@atcute/client"; +import { A, useLocation, useNavigate } from "@solidjs/router"; +import { createResource, createSignal, For, onCleanup, onMount, Show, Suspense } from "solid-js"; import { isTouchDevice } from "../layout"; +import { createDebouncedValue } from "../utils/hooks/debounced"; export const [showSearch, setShowSearch] = createSignal(false); @@ -38,11 +40,28 @@ const SearchButton = () => { const Search = () => { const navigate = useNavigate(); let searchInput!: HTMLInputElement; + const rpc = new Client({ + handler: new CredentialManager({ service: "https://public.api.bsky.app" }), + }); onMount(() => { if (useLocation().pathname !== "/") searchInput.focus(); }); + const fetchTypeahead = async (input: string) => { + if (!input.length) return []; + const res = await rpc.get("app.bsky.actor.searchActorsTypeahead", { + params: { q: input, limit: 5 }, + }); + if (res.ok) { + return res.data.actors; + } + return []; + }; + + const [input, setInput] = createSignal(); + const [search] = createResource(createDebouncedValue(input, 300), fetchTypeahead); + const processInput = (input: string) => { input = input.trim().replace(/^@/, ""); if (!input.length) return; @@ -54,6 +73,8 @@ const Search = () => { (input.startsWith("https://") || input.startsWith("http://")) ) { navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`); + } else if (search()?.length) { + navigate(`/at://${search()![0].did}`); } else { const uri = input .replace("at://", "") @@ -64,11 +85,12 @@ const Search = () => { `/at://${uriParts[0]}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`, ); } + setShowSearch(false); }; return (
{ e.preventDefault(); processInput(searchInput.value); @@ -85,12 +107,37 @@ const Search = () => { ref={searchInput} id="input" class="grow select-none placeholder:text-sm focus:outline-none" + value={input() ?? ""} + onInput={(e) => setInput(e.currentTarget.value)} /> - + + +
+ +
+ Loading...
}> + + {(actor) => ( + setShowSearch(false)} + > + + {actor.handle} + + )} + + +
+ ); }; diff --git a/src/utils/hooks/debounced.ts b/src/utils/hooks/debounced.ts new file mode 100644 index 0000000..52a42d3 --- /dev/null +++ b/src/utils/hooks/debounced.ts @@ -0,0 +1,23 @@ +import { type Accessor, createEffect, createSignal, onCleanup } from 'solid-js'; + +export const createDebouncedValue = ( + accessor: Accessor, + delay: number, + equals?: false | ((prev: T, next: T) => boolean), +): Accessor => { + const initial = accessor(); + const [state, setState] = createSignal(initial, { equals }); + + createEffect((prev: T) => { + const next = accessor(); + + if (prev !== next) { + const timeout = setTimeout(() => setState(() => next), delay); + onCleanup(() => clearTimeout(timeout)); + } + + return next; + }, initial); + + return state; +};