diff --git a/components/LoginButton.tsx b/components/LoginButton.tsx index 8e3213f2..35ec9f27 100644 --- a/components/LoginButton.tsx +++ b/components/LoginButton.tsx @@ -6,10 +6,8 @@ import { ButtonPrimary } from "./Buttons"; import { ActionButton } from "./ActionBar/ActionButton"; import { AccountSmall } from "./Icons/AccountSmall"; import { useIsMobile } from "src/hooks/isMobile"; -import { - AtmosphericHandleInfo, - HandleInput, -} from "./Subscribe/HandleSubscribe"; +import { AtmosphericHandleInfo } from "./Subscribe/HandleSubscribe"; +import { HandleInput } from "./Subscribe/HandleInput"; import { EmailInput, EmailConfirm } from "./Subscribe/EmailSubscribe"; import { useState } from "react"; import { GoToArrow } from "./Icons/GoToArrow"; diff --git a/components/Subscribe/HandleInput.tsx b/components/Subscribe/HandleInput.tsx new file mode 100644 index 00000000..119943a1 --- /dev/null +++ b/components/Subscribe/HandleInput.tsx @@ -0,0 +1,136 @@ +"use client"; +import { Agent } from "@atproto/api"; +import { Input } from "components/Input"; +import * as RadixPopover from "@radix-ui/react-popover"; +import { useState } from "react"; +import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; + +type ActorSuggestion = { + handle: string; + did: string; + displayName?: string; + avatar?: string; +}; + +export const HandleInput = (props: { + autoFocus?: boolean; + action?: React.ReactNode; + className?: string; + large?: boolean; +}) => { + let [handleValue, setHandleValue] = useState(""); + let [suggestions, setSuggestions] = useState([]); + let [dropdownOpen, setDropdownOpen] = useState(false); + let [selectedIndex, setSelectedIndex] = useState(0); + + useDebouncedEffect( + async () => { + if (!handleValue) { + setSuggestions([]); + setDropdownOpen(false); + return; + } + const agent = new Agent("https://public.api.bsky.app"); + const result = await agent.searchActorsTypeahead({ + q: handleValue, + limit: 8, + }); + const actors = result.data.actors.map((actor) => ({ + handle: actor.handle, + did: actor.did, + displayName: actor.displayName, + avatar: actor.avatar, + })); + setSuggestions(actors); + setDropdownOpen(actors.length > 0); + setSelectedIndex(0); + }, + 300, + [handleValue], + ); + + const handleSelect = (selected: string) => { + setHandleValue(selected); + setDropdownOpen(false); + setSuggestions([]); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!dropdownOpen) return; + if (e.key === "ArrowDown") { + e.preventDefault(); + setSelectedIndex((i) => Math.min(i + 1, suggestions.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setSelectedIndex((i) => Math.max(i - 1, 0)); + } else if (e.key === "Enter" && suggestions[selectedIndex]) { + e.preventDefault(); + handleSelect(suggestions[selectedIndex].handle); + } else if (e.key === "Escape") { + setDropdownOpen(false); + } + }; + + return ( + + +
+
+ @ +
+ setHandleValue(e.target.value)} + onKeyDown={handleKeyDown} + autoComplete="off" + /> + {props.action} +
+
+ + e.preventDefault()} + className="dropdownMenu z-20 bg-bg-page flex flex-col p-1 gap-1 text-primary border border-border rounded-md shadow-md w-(--radix-popover-trigger-width)" + > + {suggestions.map((actor, index) => ( + + ))} + + +
+ ); +}; diff --git a/components/Subscribe/HandleSubscribe.tsx b/components/Subscribe/HandleSubscribe.tsx index a014a6f7..1b7ae6ce 100644 --- a/components/Subscribe/HandleSubscribe.tsx +++ b/components/Subscribe/HandleSubscribe.tsx @@ -1,10 +1,9 @@ "use client"; import { ButtonPrimary } from "components/Buttons"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; -import { Input } from "components/Input"; import { Popover } from "components/Popover"; import Link from "next/link"; -import { useState } from "react"; +import { HandleInput } from "./HandleInput"; import { LogoBlacksky, LogoBluesky, @@ -68,33 +67,6 @@ export const LinkHandle = (props: { compact?: boolean }) => { ); }; -export const HandleInput = (props: { - autoFocus?: boolean; - action?: React.ReactNode; - className?: string; - large?: boolean; -}) => { - let [handleValue, setHandleValue] = useState(""); - return ( -
-
- @ -
- setHandleValue(e.target.value)} - /> - - {props.action} -
- ); -}; export const HandleInputandOAuth = (props: { link?: boolean;