This repository has no description
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;