forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
1/* eslint-disable @typescript-eslint/no-explicit-any */
2import styled from "@emotion/styled";
3import { zodResolver } from "@hookform/resolvers/zod";
4import { Search as SearchIcon } from "@styled-icons/evaicons-solid";
5import { Link as DefaultLink } from "@tanstack/react-router";
6import { Input } from "baseui/input";
7import { PLACEMENT, Popover } from "baseui/popover";
8import _ from "lodash";
9import { useEffect, useState } from "react";
10import { Controller, useForm } from "react-hook-form";
11import z from "zod";
12import Artist from "../../components/Icons/Artist";
13import Disc from "../../components/Icons/Disc";
14import Track from "../../components/Icons/Track";
15import { useSearchMutation } from "../../hooks/useSearch";
16
17const Link = styled(DefaultLink)`
18 color: initial;
19 text-decoration: none;
20`;
21
22const Header = styled.div`
23 padding: 16px;
24`;
25
26const schema = z.object({
27 keyword: z.string().nonempty(),
28});
29
30function Search() {
31 const [results, setResults] = useState<any[]>([]);
32 const { mutate, data } = useSearchMutation();
33
34 const {
35 control,
36 formState: { errors },
37 watch,
38 } = useForm({
39 mode: "onChange",
40 resolver: zodResolver(schema),
41 defaultValues: {
42 keyword: "",
43 },
44 });
45
46 const keyword = watch("keyword");
47
48 const debouncedSearch = _.debounce(async (keyword) => {
49 mutate(keyword);
50 }, 200);
51
52 useEffect(() => {
53 if (keyword.length === 0) {
54 setResults([]);
55 } else if (keyword.length > 1) {
56 debouncedSearch(keyword);
57 }
58 // eslint-disable-next-line react-hooks/exhaustive-deps
59 }, [keyword]);
60
61 useEffect(() => {
62 if (data && data.hits) {
63 setResults(data.hits);
64 } else {
65 setResults([]);
66 }
67 }, [data]);
68
69 return (
70 <>
71 <Popover
72 isOpen={keyword.length > 0 && Object.keys(errors).length === 0}
73 content={
74 <div>
75 <Header className="text-[var(--color-text)]">
76 Search for "{keyword}"
77 </Header>
78 {results.length > 0 && (
79 <div className="p-[16px] overflow-y-auto min-h-[54px] max-h-[70vh]">
80 {results.length > 0 && (
81 <>
82 {results.map((item: any) => (
83 <>
84 {item._federation.indexUid === "users" && (
85 <Link to={`/profile/${item.handle}`} key={item.id}>
86 <div className="flex flex-row mb-[10px]">
87 <img
88 key={item.did}
89 src={item.avatar}
90 alt={item.displayName}
91 className="w-[50px] h-[50px] mr-[12px] rounded-full"
92 />
93 <div>
94 <div className="overflow-hidden">
95 <div className="overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text)]">
96 {item.displayName}
97 </div>
98 </div>
99 <div className="text-[var(--color-text-muted)] text-[14px]">
100 @{item.handle}
101 </div>
102 </div>
103 </div>
104 </Link>
105 )}
106
107 {item.uri &&
108 (item.name || item.title) &&
109 item._federation.indexUid !== "users" && (
110 <Link
111 to={`/${item.uri
112 ?.split("at://")[1]
113 .replace("app.rocksky.", "")}`}
114 key={item.id}
115 >
116 <div
117 key={item.id}
118 className="h-[64px] flex flex-row items-center"
119 >
120 {item._federation.indexUid === "artists" &&
121 item.picture && (
122 <img
123 key={item.id}
124 src={item.picture}
125 alt={item.name}
126 className="w-[50px] h-[50px] mr-[12px] rounded-full"
127 />
128 )}
129 {item._federation.indexUid === "artists" &&
130 !item.picture && (
131 <div
132 key={item.id}
133 className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]"
134 >
135 <div className="w-[28px] h-[28px]">
136 <Artist color="rgba(66, 87, 108, 0.65)" />
137 </div>
138 </div>
139 )}
140 {item._federation.indexUid === "albums" &&
141 item.albumArt && (
142 <img
143 key={item.id}
144 src={item.albumArt}
145 alt={item.title}
146 className="w-[50px] h-[50px] mr-[12px]"
147 />
148 )}
149 {item._federation.indexUid === "albums" &&
150 !item.albumArt && (
151 <div className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]">
152 <div className="w-[28px] h-[28px]">
153 <Disc
154 color="rgba(66, 87, 108, 0.65)"
155 width={30}
156 height={30}
157 />
158 </div>
159 </div>
160 )}
161 {item._federation.indexUid === "tracks" &&
162 item.albumArt && (
163 <img
164 key={item.id}
165 src={item.albumArt}
166 alt={item.title}
167 className="w-[50px] h-[50px] mr-[12px]"
168 />
169 )}
170 {item._federation.indexUid === "tracks" &&
171 !item.albumArt && (
172 <div className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]">
173 <div className="w-[28px] h-[28px]">
174 <Track color="rgba(66, 87, 108, 0.65)" />
175 </div>
176 </div>
177 )}
178 <div className="overflow-hidden w-[calc(100%-70px)]">
179 <div className="overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text)]">
180 {item.name || item.title}
181 </div>
182 {item._federation.indexUid === "tracks" && (
183 <div className="text-[14px] text-[var(--color-text-muted)]">
184 Track
185 </div>
186 )}
187 {item._federation.indexUid === "albums" && (
188 <div className="text-[14px] text-[var(--color-text-muted)]">
189 Album
190 </div>
191 )}
192 {item._federation.indexUid === "artists" && (
193 <div className="text-[var(--color-text-muted)] text-[14px]">
194 Artist
195 </div>
196 )}
197 </div>
198 </div>
199 </Link>
200 )}
201 {!item.uri &&
202 (item.name || item.title) &&
203 item._federation.indexUid !== "users" && (
204 <div>
205 <div
206 key={item.id}
207 className="h-[64px] flex flex-row items-center"
208 >
209 {item._federation.indexUid === "artists" &&
210 item.picture && (
211 <img
212 src={item.picture}
213 alt={item.name}
214 className="w-[50px] h-[50px] mr-[12px] rounded-full"
215 />
216 )}
217 {item._federation.indexUid === "artists" &&
218 !item.picture && (
219 <div className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]">
220 <div className="w-[28px] h-[28px]">
221 <Artist color="rgba(66, 87, 108, 0.65)" />
222 </div>
223 </div>
224 )}
225 {item._federation.indexUid === "albums" &&
226 item.albumArt && (
227 <img
228 src={item.albumArt}
229 alt={item.title}
230 className="w-[50px] h-[50px] mr-[12px]"
231 />
232 )}
233 {item._federation.indexUid === "tracks" && (
234 <img
235 src={item.albumArt}
236 alt={item.title}
237 className="w-[50px] h-[50px] mr-[12px]"
238 />
239 )}
240 {["artists", "albums", "tracks"].includes(
241 item._federation.indexUid,
242 ) && (
243 <div className="overflow-hidden">
244 <div className="overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text)]">
245 {item.name || item.title}
246 </div>
247 {item._federation.indexUid === "tracks" && (
248 <div className="text-[14px] text-[var(--color-text-muted)]">
249 Track
250 </div>
251 )}
252 {item._federation.indexUid === "albums" && (
253 <div className="text-[14px] text-[var(--color-text-muted)]">
254 Album
255 </div>
256 )}
257 {item._federation.indexUid ===
258 "artists" && (
259 <div className="text-[14px] text-[var(--color-text-muted)]">
260 Artist
261 </div>
262 )}
263 </div>
264 )}
265 </div>
266 </div>
267 )}
268 </>
269 ))}
270 </>
271 )}
272 </div>
273 )}
274 </div>
275 }
276 placement={PLACEMENT.bottom}
277 overrides={{
278 Body: {
279 style: {
280 backgroundColor: "var(--color-background)",
281 width: "300px",
282 border: "0.5px solid var(--color-border) !important",
283 },
284 },
285 Inner: {
286 style: {
287 backgroundColor: "var(--color-background)",
288 },
289 },
290 }}
291 >
292 <div>
293 <Controller
294 name="keyword"
295 control={control}
296 render={({ field }) => (
297 <Input
298 startEnhancer={
299 <SearchIcon size={20} color="var(--color-text-muted)" />
300 }
301 placeholder="Search"
302 clearable
303 clearOnEscape
304 overrides={{
305 Root: {
306 style: {
307 backgroundColor: "var(--color-input-background)",
308 borderColor: "var(--color-input-background)",
309 },
310 },
311 StartEnhancer: {
312 style: {
313 backgroundColor: "var(--color-input-background)",
314 },
315 },
316 InputContainer: {
317 style: {
318 backgroundColor: "var(--color-input-background)",
319 },
320 },
321 Input: {
322 style: {
323 color: "var(--color-text)",
324 caretColor: "var(--color-text)",
325 },
326 },
327 ClearIcon: {
328 style: {
329 color: "var(--color-clear-input) !important",
330 },
331 },
332 }}
333 {...field}
334 />
335 )}
336 />
337 </div>
338 </Popover>
339 </>
340 );
341}
342
343export default Search;