Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿
at main 160 lines 5.2 kB view raw
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;