A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 343 lines 15 kB view raw
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;