This repository has no description
0

Configure Feed

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

add auto-complete to search bar

+322 -53
public/default-avatar.png

This is a binary file and will not be displayed.

+133 -40
src/components/SearchBar/SearchBar.css
··· 1 1 /* src/components/SearchBar/SearchBar.css */ 2 2 3 + .search-bar-container { 4 + position: relative; 5 + width: 100%; 6 + max-width: 400px; /* Adjust as needed */ 7 + margin: 0 auto; 8 + } 9 + 3 10 .search-bar { 4 - display: flex; 5 - justify-content: center; 6 - margin: 20px 0; 7 - } 8 - 9 - .search-bar input { 10 - padding: 10px; 11 - font-size: 1em; 12 - border: 1px solid #ccc; 13 - border-radius: 5px 0 0 5px; 14 - border-right: 0px; 15 - } 16 - 17 - .search-bar button { 18 - padding: 10px 20px; 19 - font-size: 1em; 20 - border: none; 21 - background-color: #007bff; 22 - color: white; 23 - border-radius: 0 5px 5px 0; 24 - cursor: pointer; 25 - border: 1px solid #007bff; 26 - } 27 - 28 - .search-bar button:hover { 29 - background-color: #0056b3; 30 - } 11 + display: flex; 12 + width: 100%; 13 + /* Existing styles */ 14 + padding: 10px; 15 + flex-direction: row; 16 + gap: 0px; 17 + } 18 + 19 + .search-bar input { 20 + flex: 1; 21 + padding: 10px; 22 + font-size: 1em; 23 + border: 1px solid #ccc; 24 + border-radius: 5px 0 0 5px; 25 + border-right: 0px; 26 + } 27 + 28 + .search-bar button { 29 + padding: 10px 20px; 30 + font-size: 1em; 31 + border: none; 32 + background-color: #007bff; 33 + color: white; 34 + border-radius: 0 5px 5px 0; 35 + cursor: pointer; 36 + border: 1px solid #007bff; 37 + } 38 + 39 + .search-bar button:hover { 40 + background-color: #0056b3; 41 + } 42 + 43 + input:focus-visible { 44 + outline: 0px; 45 + } 46 + 47 + form { 48 + padding: 20px 20px; 49 + flex-direction: unset; 50 + gap: 0px; 51 + } 52 + 53 + .dark-mode form { 54 + background-color: #292929; 55 + border: 1px solid #3f3f3f; 56 + } 57 + 58 + /* New styles for suggestions */ 59 + .suggestions-list { 60 + position: absolute; 61 + top: 100%; 62 + left: 0; 63 + right: 0; 64 + background-color: white; 65 + border: 1px solid #ccc; 66 + border-top: none; 67 + max-height: 200px; 68 + overflow-y: auto; 69 + z-index: 1000; 70 + list-style: none; 71 + margin: 0; 72 + padding: 0; 73 + } 74 + 75 + .suggestion-item { 76 + display: flex; 77 + align-items: center; 78 + padding: 8px 12px; 79 + cursor: pointer; 80 + } 81 + 82 + .suggestion-item:hover, 83 + .suggestion-item.active, 84 + .suggestion-item[aria-selected="true"] { 85 + background-color: #f0f0f0; 86 + } 87 + 88 + .suggestion-avatar { 89 + width: 32px; 90 + height: 32px; 91 + border-radius: 50%; 92 + object-fit: cover; 93 + margin-right: 10px; 94 + } 31 95 32 - input:focus-visible { 33 - outline: 0px; 34 - } 96 + .suggestion-handle { 97 + font-size: 1em; 98 + color: #333; 99 + } 35 100 36 - form { 37 - padding: 20px 20px; 38 - flex-direction: unset; 39 - gap: 0px; 40 - } 41 - 42 - .dark-mode form { 43 - background-color: #292929; 44 - border: 1px solid #3f3f3f; 45 - } 101 + .loading { 102 + position: absolute; 103 + top: 100%; 104 + left: 0; 105 + padding: 8px 12px; 106 + background-color: white; 107 + border: 1px solid #ccc; 108 + border-top: none; 109 + width: 100%; 110 + z-index: 1000; 111 + font-size: 0.9em; 112 + color: #555; 113 + } 114 + 115 + /* Visually hidden class for accessibility */ 116 + .sr-only { 117 + position: absolute; 118 + width: 1px; 119 + height: 1px; 120 + padding: 0; 121 + margin: -1px; 122 + overflow: hidden; 123 + clip: rect(0, 0, 0, 0); 124 + white-space: nowrap; 125 + border: 0; 126 + } 127 + 128 + .dark-mode .suggestions-list { 129 + background-color: #3f3f3f; 130 + color: white; 131 + border-color: #555; 132 + } 133 + 134 + .dark-mode .suggestion-item:hover, 135 + .dark-mode .suggestion-item.active, 136 + .dark-mode .suggestion-item[aria-selected="true"] { 137 + background-color: #555; 138 + }
+189 -13
src/components/SearchBar/SearchBar.js
··· 1 1 // src/components/SearchBar/SearchBar.jsx 2 2 3 - import React, { useState } from "react"; 3 + import React, { useState, useEffect, useRef, useCallback } from "react"; 4 4 import { useNavigate } from "react-router-dom"; 5 - import "./SearchBar.css"; // Create corresponding CSS for styling 5 + import "./SearchBar.css"; 6 6 7 7 const SearchBar = () => { 8 8 const [username, setUsername] = useState(""); 9 + const [suggestions, setSuggestions] = useState([]); 10 + const [showSuggestions, setShowSuggestions] = useState(false); 11 + const [isLoading, setIsLoading] = useState(false); 12 + const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(-1); // New state 9 13 const navigate = useNavigate(); 14 + const debounceTimeout = useRef(null); 15 + const suggestionsRef = useRef(null); 16 + 17 + // Fetch suggestions from the API 18 + const fetchSuggestions = useCallback(async (query) => { 19 + if (!query) { 20 + setSuggestions([]); 21 + return; 22 + } 23 + 24 + setIsLoading(true); 25 + try { 26 + const response = await fetch( 27 + `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent( 28 + query 29 + )}&limit=5` 30 + ); 31 + 32 + if (!response.ok) { 33 + console.error("Error fetching suggestions:", response.statusText); 34 + setSuggestions([]); 35 + return; 36 + } 37 + 38 + const data = await response.json(); 39 + setSuggestions(data.actors || []); 40 + } catch (error) { 41 + console.error("Error fetching suggestions:", error); 42 + setSuggestions([]); 43 + } finally { 44 + setIsLoading(false); 45 + } 46 + }, []); 47 + 48 + // Debounce the API call to prevent excessive requests 49 + useEffect(() => { 50 + if (debounceTimeout.current) { 51 + clearTimeout(debounceTimeout.current); 52 + } 53 + 54 + debounceTimeout.current = setTimeout(() => { 55 + fetchSuggestions(username.trim()); 56 + }, 300); // 300ms debounce delay 57 + 58 + return () => { 59 + if (debounceTimeout.current) { 60 + clearTimeout(debounceTimeout.current); 61 + } 62 + }; 63 + }, [username, fetchSuggestions]); 64 + 65 + // Handle clicks outside the suggestions dropdown to close it 66 + useEffect(() => { 67 + const handleClickOutside = (event) => { 68 + if ( 69 + suggestionsRef.current && 70 + !suggestionsRef.current.contains(event.target) 71 + ) { 72 + setShowSuggestions(false); 73 + setActiveSuggestionIndex(-1); // Reset active suggestion 74 + } 75 + }; 76 + 77 + document.addEventListener("mousedown", handleClickOutside); 78 + return () => { 79 + document.removeEventListener("mousedown", handleClickOutside); 80 + }; 81 + }, []); 10 82 11 83 const handleSubmit = (e) => { 12 84 e.preventDefault(); 13 85 if (username.trim() !== "") { 14 - // Encode the username to safely include in the URL 15 86 const encodedUsername = encodeURIComponent(username.trim()); 16 87 navigate(`/${encodedUsername}`); 17 88 setUsername(""); 89 + setSuggestions([]); 90 + setShowSuggestions(false); 91 + setActiveSuggestionIndex(-1); 92 + } 93 + }; 94 + 95 + const handleSuggestionClick = (handle) => { 96 + setUsername(handle); 97 + setSuggestions([]); 98 + setShowSuggestions(false); 99 + setActiveSuggestionIndex(-1); 100 + // Optionally, navigate immediately upon selection 101 + navigate(`/${encodeURIComponent(handle)}`); 102 + }; 103 + 104 + const handleInputChange = (e) => { 105 + setUsername(e.target.value); 106 + setShowSuggestions(true); 107 + setActiveSuggestionIndex(-1); // Reset active suggestion on input change 108 + }; 109 + 110 + // Handle key down events for keyboard navigation 111 + const handleKeyDown = (e) => { 112 + if (e.key === "ArrowDown") { 113 + e.preventDefault(); 114 + if (suggestions.length > 0) { 115 + setActiveSuggestionIndex((prevIndex) => 116 + prevIndex < suggestions.length - 1 ? prevIndex + 1 : 0 117 + ); 118 + } 119 + } else if (e.key === "ArrowUp") { 120 + e.preventDefault(); 121 + if (suggestions.length > 0) { 122 + setActiveSuggestionIndex((prevIndex) => 123 + prevIndex > 0 ? prevIndex - 1 : suggestions.length - 1 124 + ); 125 + } 126 + } else if (e.key === "Enter") { 127 + if (activeSuggestionIndex >= 0 && activeSuggestionIndex < suggestions.length) { 128 + e.preventDefault(); 129 + handleSuggestionClick(suggestions[activeSuggestionIndex].handle); 130 + } 131 + } else if (e.key === "Escape") { 132 + setShowSuggestions(false); 133 + setActiveSuggestionIndex(-1); 18 134 } 19 135 }; 20 136 21 137 return ( 22 - <form className="search-bar" onSubmit={handleSubmit}> 23 - <input 24 - type="text" 25 - placeholder="(e.g. dame.bsky.social)" 26 - value={username} 27 - onChange={(e) => setUsername(e.target.value)} 28 - required 29 - /> 30 - <button type="submit">Search</button> 31 - </form> 138 + <div className="search-bar-container" ref={suggestionsRef}> 139 + <form className="search-bar" onSubmit={handleSubmit} role="search"> 140 + <input 141 + type="text" 142 + placeholder="(e.g. dame.bsky.social)" 143 + value={username} 144 + onChange={handleInputChange} 145 + required 146 + onFocus={() => { 147 + if (suggestions.length > 0) setShowSuggestions(true); 148 + }} 149 + onKeyDown={handleKeyDown} // Added key down handler 150 + role="combobox" 151 + aria-autocomplete="list" 152 + aria-controls="suggestions-list" 153 + aria-expanded={showSuggestions} 154 + aria-haspopup="listbox" 155 + aria-activedescendant={ 156 + activeSuggestionIndex >= 0 157 + ? `suggestion-${activeSuggestionIndex}` 158 + : undefined 159 + } 160 + /> 161 + <button type="submit">Search</button> 162 + </form> 163 + {showSuggestions && suggestions.length > 0 && ( 164 + <ul 165 + className="suggestions-list" 166 + id="suggestions-list" 167 + role="listbox" 168 + aria-label="Username suggestions" 169 + > 170 + {suggestions.map((actor, index) => ( 171 + <li 172 + key={actor.did} 173 + id={`suggestion-${index}`} 174 + className={`suggestion-item ${ 175 + index === activeSuggestionIndex ? "active" : "" 176 + }`} 177 + role="option" 178 + aria-selected={index === activeSuggestionIndex} 179 + onClick={() => handleSuggestionClick(actor.handle)} 180 + onMouseEnter={() => setActiveSuggestionIndex(index)} // Optional: highlight on hover 181 + > 182 + <img 183 + src={actor.avatar} 184 + alt={`${actor.handle} avatar`} 185 + className="suggestion-avatar" 186 + onError={(e) => { 187 + e.target.onerror = null; 188 + e.target.src = "/default-avatar.png"; // Fallback avatar 189 + }} 190 + /> 191 + <span className="suggestion-handle">{actor.handle}</span> 192 + </li> 193 + ))} 194 + </ul> 195 + )} 196 + {isLoading && <div className="loading">Loading...</div>} 197 + {/* Accessible live region for screen readers */} 198 + <div 199 + role="status" 200 + aria-live="polite" 201 + className="sr-only" 202 + > 203 + {suggestions.length > 0 204 + ? `${suggestions.length} suggestions available.` 205 + : "No suggestions available."} 206 + </div> 207 + </div> 32 208 ); 33 209 }; 34 210