This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 6.4 kB View raw
1import React, { useState, useEffect, useRef } from "react"; 2import { useNavigate } from "react-router-dom"; 3import { isDID, resolveDIDToHandle } from "../../utils/didUtils"; 4import "./SearchBar.css"; 5 6const SearchBar = () => { 7 const [username, setUsername] = useState(""); 8 const [suggestions, setSuggestions] = useState([]); 9 const [autocompleteActive, setAutocompleteActive] = useState(false); 10 const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(-1); 11 const [selectedSuggestion, setSelectedSuggestion] = useState(''); 12 const [isLoading, setIsLoading] = useState(false); 13 const navigate = useNavigate(); 14 const debounceTimeout = useRef(null); 15 16 // Debounce function 17 const debounce = (func, delay) => { 18 let timer; 19 const debounced = (...args) => { 20 clearTimeout(timer); 21 timer = setTimeout(() => func(...args), delay); 22 }; 23 debounced.cancel = () => { 24 clearTimeout(timer); 25 }; 26 return debounced; 27 }; 28 29 // Fetch suggestions from the API 30 const fetchSuggestions = async (query) => { 31 if (!query || isDID(query)) { 32 setSuggestions([]); 33 return; 34 } 35 36 setIsLoading(true); 37 try { 38 const res = await fetch( 39 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5` 40 ); 41 if (!res.ok) throw new Error("Failed to fetch suggestions"); 42 const data = await res.json(); 43 setSuggestions(data.actors || []); 44 setAutocompleteActive(true); 45 } catch (error) { 46 console.error("Error fetching suggestions:", error); 47 setSuggestions([]); 48 } finally { 49 setIsLoading(false); 50 } 51 }; 52 53 const debouncedFetchSuggestions = useRef(debounce(fetchSuggestions, 300)).current; 54 55 useEffect(() => { 56 // Only fetch suggestions if the username does NOT match a selected suggestion 57 if (!selectedSuggestion) { 58 debouncedFetchSuggestions(username); 59 } 60 return () => { 61 debouncedFetchSuggestions.cancel(); 62 }; 63 }, [username, debouncedFetchSuggestions, selectedSuggestion]); 64 65 const handleNavigation = async (handle) => { 66 // First, navigate to home to reset any error states 67 navigate("/home"); 68 69 try { 70 // If the handle is a DID, resolve it first 71 if (isDID(handle)) { 72 const resolvedHandle = await resolveDIDToHandle(handle); 73 if (resolvedHandle) { 74 handle = resolvedHandle; 75 } 76 } 77 78 // Then, after a brief timeout, navigate to the profile 79 setTimeout(() => { 80 navigate(`/${encodeURIComponent(handle)}`); 81 }, 0); 82 } catch (error) { 83 console.error("Error resolving DID:", error); 84 // Navigate to the original handle if DID resolution fails 85 setTimeout(() => { 86 navigate(`/${encodeURIComponent(handle)}`); 87 }, 0); 88 } 89 }; 90 91 const handleSubmit = (e) => { 92 e.preventDefault(); 93 if (username.trim() !== "") { 94 handleNavigation(username.trim()); 95 setUsername(""); 96 setSuggestions([]); 97 setAutocompleteActive(false); 98 setActiveSuggestionIndex(-1); 99 } 100 }; 101 102 const handleInputChange = (e) => { 103 setUsername(e.target.value); 104 if (selectedSuggestion && e.target.value !== selectedSuggestion) { 105 setSelectedSuggestion(''); 106 } 107 }; 108 109 const handleKeyDown = (e) => { 110 if (!autocompleteActive) return; 111 112 switch (e.key) { 113 case "ArrowDown": 114 e.preventDefault(); 115 setActiveSuggestionIndex(prev => 116 prev < suggestions.length - 1 ? prev + 1 : 0 117 ); 118 break; 119 case "ArrowUp": 120 e.preventDefault(); 121 setActiveSuggestionIndex(prev => 122 prev > 0 ? prev - 1 : suggestions.length - 1 123 ); 124 break; 125 case "Enter": 126 if (activeSuggestionIndex >= 0 && activeSuggestionIndex < suggestions.length) { 127 e.preventDefault(); 128 const selectedHandle = suggestions[activeSuggestionIndex].handle; 129 setUsername(selectedHandle); 130 setSelectedSuggestion(selectedHandle); 131 setSuggestions([]); 132 setAutocompleteActive(false); 133 handleNavigation(selectedHandle); 134 } 135 break; 136 case "Escape": 137 setAutocompleteActive(false); 138 setActiveSuggestionIndex(-1); 139 break; 140 default: 141 break; 142 } 143 }; 144 145 return ( 146 <div className="search-bar-container"> 147 <form className="search-bar" onSubmit={handleSubmit} role="search"> 148 <div style={{ position: 'relative' }}> 149 <input 150 type="text" 151 placeholder="(e.g. user.bsky.social)" 152 value={username} 153 onChange={handleInputChange} 154 onKeyDown={handleKeyDown} 155 required 156 role="combobox" 157 aria-autocomplete="list" 158 aria-controls="autocomplete-items" 159 aria-expanded={autocompleteActive} 160 aria-haspopup="listbox" 161 aria-activedescendant={ 162 activeSuggestionIndex >= 0 163 ? `suggestion-${activeSuggestionIndex}` 164 : undefined 165 } 166 /> 167 {autocompleteActive && suggestions.length > 0 && ( 168 <div className="autocomplete-items" id="autocomplete-items"> 169 {suggestions.map((actor, index) => ( 170 <div 171 key={actor.handle} 172 className={`autocomplete-item ${index === activeSuggestionIndex ? 'active' : ''}`} 173 onClick={() => { 174 setUsername(actor.handle); 175 setSelectedSuggestion(actor.handle); 176 setSuggestions([]); 177 setAutocompleteActive(false); 178 debouncedFetchSuggestions.cancel(); 179 handleNavigation(actor.handle); 180 }} 181 > 182 <img 183 src={actor.avatar} 184 alt={`${actor.handle}'s avatar`} 185 onError={(e) => { 186 e.target.onerror = null; 187 e.target.src = "/default-avatar.png"; 188 }} 189 /> 190 <span>{actor.handle}</span> 191 </div> 192 ))} 193 </div> 194 )} 195 </div> 196 <button type="submit">Search</button> 197 </form> 198 </div> 199 ); 200}; 201 202export default SearchBar;