This repository has no description
0

Configure Feed

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

1'use client'; 2 3import React, { useState, useEffect, useRef } from 'react'; 4import { useRouter } from 'next/navigation'; 5import styles from './ProfileSearch.module.css'; 6 7type UserSuggestion = { 8 did: string; 9 handle: string; 10 displayName?: string; 11 avatar?: string | null; 12}; 13 14export default function ProfileSearch() { 15 const [query, setQuery] = useState(''); 16 const [suggestions, setSuggestions] = useState<UserSuggestion[]>([]); 17 const [loading, setLoading] = useState(false); 18 const [showSuggestions, setShowSuggestions] = useState(false); 19 const [placeholder, setPlaceholder] = useState('Search user @handle'); 20 const suggestionsRef = useRef<HTMLDivElement>(null); 21 const inputRef = useRef<HTMLInputElement>(null); 22 const router = useRouter(); 23 const debounceTimerRef = useRef<NodeJS.Timeout | null>(null); 24 25 // Update placeholder text based on screen width 26 useEffect(() => { 27 const updatePlaceholder = () => { 28 if (window.innerWidth <= 480) { 29 setPlaceholder('Search handle'); 30 } else { 31 setPlaceholder('Search handle'); 32 } 33 }; 34 35 // Initial check 36 updatePlaceholder(); 37 38 // Listen for resize events 39 window.addEventListener('resize', updatePlaceholder); 40 41 // Cleanup 42 return () => window.removeEventListener('resize', updatePlaceholder); 43 }, []); 44 45 // Close suggestions when clicking outside 46 useEffect(() => { 47 const handleClickOutside = (event: MouseEvent) => { 48 if ( 49 suggestionsRef.current && 50 !suggestionsRef.current.contains(event.target as Node) && 51 !inputRef.current?.contains(event.target as Node) 52 ) { 53 setShowSuggestions(false); 54 } 55 }; 56 57 document.addEventListener('mousedown', handleClickOutside); 58 return () => { 59 document.removeEventListener('mousedown', handleClickOutside); 60 }; 61 }, []); 62 63 // Enable suggestions with debouncing 64 useEffect(() => { 65 // Clear previous timer if it exists 66 if (debounceTimerRef.current) { 67 clearTimeout(debounceTimerRef.current); 68 } 69 70 // Don't search for very short queries 71 if (!query || query.length < 2) { 72 setSuggestions([]); 73 setShowSuggestions(false); 74 return; 75 } 76 77 // Set a debounce timer to avoid too many requests 78 debounceTimerRef.current = setTimeout(async () => { 79 try { 80 setLoading(true); 81 82 // Format the query - remove @ if it exists 83 const searchQuery = query.trim().startsWith('@') 84 ? query.trim().substring(1) 85 : query.trim(); 86 87 // Call the Bluesky API for typeahead suggestions 88 const response = await fetch( 89 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(searchQuery)}&limit=5` 90 ); 91 92 if (response.ok) { 93 const data = await response.json(); 94 if (data.actors && Array.isArray(data.actors)) { 95 // Map to our UserSuggestion type 96 setSuggestions(data.actors.map((actor: any) => ({ 97 did: actor.did, 98 handle: actor.handle, 99 displayName: actor.displayName, 100 avatar: actor.avatar 101 }))); 102 setShowSuggestions(true); 103 } 104 } else { 105 console.error('Failed to fetch suggestions:', await response.text()); 106 } 107 } catch (error) { 108 console.error('Error fetching suggestions:', error); 109 } finally { 110 setLoading(false); 111 } 112 }, 300); // 300ms debounce delay 113 114 return () => { 115 if (debounceTimerRef.current) { 116 clearTimeout(debounceTimerRef.current); 117 } 118 }; 119 }, [query]); 120 121 const handleSearch = (e: React.FormEvent) => { 122 e.preventDefault(); 123 if (query.trim()) { 124 // Normalize the handle by removing @ if present 125 const handle = query.trim().startsWith('@') 126 ? query.trim().substring(1) 127 : query.trim(); 128 129 router.push(`/profile/${handle}`); 130 setShowSuggestions(false); 131 } 132 }; 133 134 // Handle clicking on a suggestion 135 const handleSuggestionClick = (suggestion: UserSuggestion) => { 136 router.push(`/profile/${suggestion.handle}`); 137 setShowSuggestions(false); 138 setQuery(''); // Clear the input 139 }; 140 141 return ( 142 <div className={styles.searchContainer}> 143 <form onSubmit={handleSearch} className={styles.searchForm}> 144 <input 145 ref={inputRef} 146 type="text" 147 value={query} 148 onChange={(e) => setQuery(e.target.value)} 149 placeholder={placeholder} 150 className={`${styles.searchInput} font-regular`} 151 aria-label="Search for a user profile" 152 /> 153 <button type="submit" className={`${styles.searchButton} font-medium`}> 154 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 155 <circle cx="11" cy="11" r="8"></circle> 156 <line x1="21" y1="21" x2="16.65" y2="16.65"></line> 157 </svg> 158 </button> 159 </form> 160 161 {/* Suggestions dropdown */} 162 {showSuggestions && ( 163 <div className={styles.suggestionsContainer} ref={suggestionsRef}> 164 {loading ? ( 165 <div className={styles.loadingContainer}> 166 <div className={styles.loadingDot}></div> 167 <div className={styles.loadingDot}></div> 168 <div className={styles.loadingDot}></div> 169 </div> 170 ) : suggestions.length > 0 ? ( 171 <ul className={styles.suggestionsList}> 172 {suggestions.map((suggestion) => ( 173 <li key={suggestion.did} className={styles.suggestionItem}> 174 <button 175 type="button" 176 className={styles.suggestionButton} 177 onClick={() => handleSuggestionClick(suggestion)} 178 > 179 {suggestion.avatar ? ( 180 <img 181 src={suggestion.avatar} 182 alt={suggestion.handle} 183 className={styles.avatar} 184 width={28} 185 height={28} 186 /> 187 ) : ( 188 <div className={styles.avatarPlaceholder}></div> 189 )} 190 <div className={styles.suggestionInfo}> 191 <span className={`${styles.handle} font-medium`}>@{suggestion.handle}</span> 192 </div> 193 </button> 194 </li> 195 ))} 196 </ul> 197 ) : ( 198 <div className={styles.noResults}>No results found</div> 199 )} 200 </div> 201 )} 202 </div> 203 ); 204}