Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿
1import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
2import getAccount from "@hey/helpers/getAccount";
3import {
4 type AccountFragment,
5 AccountsOrderBy,
6 type AccountsRequest,
7 PageSize,
8 useAccountsLazyQuery
9} from "@hey/indexer";
10import { useClickAway, useDebounce } from "@uidotdev/usehooks";
11import type { MutableRefObject } from "react";
12import { useCallback, useEffect, useState } from "react";
13import { useLocation, useNavigate, useSearchParams } from "react-router";
14import { z } from "zod";
15import SingleAccount from "@/components/Shared/Account/SingleAccount";
16import Loader from "@/components/Shared/Loader";
17import { Card, Form, Input, useZodForm } from "@/components/Shared/UI";
18import cn from "@/helpers/cn";
19import { useAccountLinkStore } from "@/store/non-persisted/navigation/useAccountLinkStore";
20import { useSearchStore } from "@/store/persisted/useSearchStore";
21import RecentAccounts from "./RecentAccounts";
22
23interface SearchProps {
24 placeholder?: string;
25}
26
27const ValidationSchema = z.object({
28 query: z
29 .string()
30 .trim()
31 .min(1, { message: "Enter something to search" })
32 .max(100, { message: "Query should not exceed 100 characters" })
33});
34
35const Search = ({ placeholder = "Search…" }: SearchProps) => {
36 const { pathname } = useLocation();
37 const navigate = useNavigate();
38 const [searchParams] = useSearchParams();
39 const type = searchParams.get("type");
40 const { setCachedAccount } = useAccountLinkStore();
41 const { addAccount } = useSearchStore();
42 const [showDropdown, setShowDropdown] = useState(false);
43 const [accounts, setAccounts] = useState<AccountFragment[]>([]);
44
45 const form = useZodForm({
46 defaultValues: { query: "" },
47 schema: ValidationSchema
48 });
49
50 const query = form.watch("query");
51 const debouncedSearchText = useDebounce<string>(query, 500);
52
53 const handleReset = useCallback(() => {
54 setShowDropdown(false);
55 setAccounts([]);
56 form.reset();
57 }, [form]);
58
59 const dropdownRef = useClickAway(() => {
60 handleReset();
61 }) as MutableRefObject<HTMLDivElement>;
62
63 const [searchAccounts, { loading }] = useAccountsLazyQuery();
64
65 const handleSubmit = useCallback(
66 ({ query }: z.infer<typeof ValidationSchema>) => {
67 const search = query.trim();
68 if (pathname === "/search") {
69 navigate(`/search?q=${encodeURIComponent(search)}&type=${type}`);
70 } else {
71 navigate(`/search?q=${encodeURIComponent(search)}&type=accounts`);
72 }
73 handleReset();
74 },
75 [pathname, navigate, type, handleReset]
76 );
77
78 const handleShowDropdown = useCallback(() => {
79 setShowDropdown(true);
80 }, []);
81
82 useEffect(() => {
83 if (pathname !== "/search" && showDropdown && debouncedSearchText) {
84 const request: AccountsRequest = {
85 filter: { searchBy: { localNameQuery: debouncedSearchText } },
86 orderBy: AccountsOrderBy.BestMatch,
87 pageSize: PageSize.Fifty
88 };
89
90 searchAccounts({ variables: { request } }).then((res) => {
91 if (res.data?.accounts?.items) {
92 setAccounts(res.data.accounts.items);
93 }
94 });
95 }
96 }, [debouncedSearchText]);
97
98 return (
99 <div className="w-full">
100 <Form form={form} onSubmit={handleSubmit}>
101 <Input
102 className="px-3 py-3 text-sm"
103 iconLeft={<MagnifyingGlassIcon />}
104 iconRight={
105 <XMarkIcon
106 className={cn("cursor-pointer", query ? "visible" : "invisible")}
107 onClick={handleReset}
108 />
109 }
110 onClick={handleShowDropdown}
111 placeholder={placeholder}
112 type="text"
113 {...form.register("query")}
114 />
115 </Form>
116 {pathname !== "/search" && showDropdown ? (
117 <div className="fixed z-10 mt-2 w-[360px]" ref={dropdownRef}>
118 <Card className="max-h-[80vh] overflow-y-auto py-2">
119 {!debouncedSearchText && (
120 <RecentAccounts onAccountClick={handleReset} />
121 )}
122 {loading ? (
123 <Loader className="my-3" message="Searching users" small />
124 ) : (
125 <>
126 {accounts.map((account) => (
127 <div
128 className="cursor-pointer px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-800"
129 key={account.address}
130 onClick={() => {
131 setCachedAccount(account);
132 addAccount(account.address);
133 navigate(getAccount(account).link);
134 handleReset();
135 }}
136 >
137 <SingleAccount
138 account={account}
139 hideFollowButton
140 hideUnfollowButton
141 linkToAccount={false}
142 showUserPreview={false}
143 />
144 </div>
145 ))}
146 {accounts.length ? null : (
147 <div className="px-4 py-2">
148 Try searching for people or keywords
149 </div>
150 )}
151 </>
152 )}
153 </Card>
154 </div>
155 ) : null}
156 </div>
157 );
158};
159
160export default Search;