diff --git a/src/components/Verifier/Verifier.css b/src/components/Verifier/Verifier.css index 91d77f2..d25dd2d 100644 --- a/src/components/Verifier/Verifier.css +++ b/src/components/Verifier/Verifier.css @@ -380,4 +380,87 @@ .verifier-verified-status { color: var(--success-text, green); } .verifier-not-verified-status { color: var(--text-muted, grey); } .verifier-error-status { color: var(--error-text, red); } -.verifier-checking-status, .verifier-idle-status { color: var(--text-muted, grey); } \ No newline at end of file +.verifier-checking-status, .verifier-idle-status { color: var(--text-muted, grey); } + +/* Styles for Typeahead/Autocomplete Suggestions */ +.verifier-input-wrapper { + position: relative; /* Required for absolute positioning of suggestions */ +} + +.verifier-suggestions-list { + list-style: none; + padding: 0; + margin: 5px 0 0 0; + position: absolute; + top: 100%; /* Position below the input */ + left: 0; + right: 0; + background-color: var(--navbar-bg); /* Match nearby elements */ + border: 1px solid var(--card-border); + border-top: none; /* Avoid double border with input */ + border-radius: 0 0 6px 6px; /* Round bottom corners */ + max-height: 250px; /* Limit height and allow scroll */ + overflow-y: auto; + z-index: 1000; /* Ensure it appears above other content */ + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.verifier-suggestion-item { + display: flex; + align-items: center; + padding: 10px 15px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.verifier-suggestion-item:hover { + background-color: var(--button-hover-bg); /* Use button hover for consistency */ + color: var(--button-text); +} + +.verifier-suggestion-item.loading, +.verifier-suggestion-item.none { + font-style: italic; + color: var(--text-muted); + cursor: default; +} + +.verifier-suggestion-item:hover.loading, +.verifier-suggestion-item:hover.none { + background-color: transparent; /* Don't highlight loading/none items */ + color: var(--text-muted); +} + +.verifier-suggestion-avatar { + width: 30px; + height: 30px; + border-radius: 50%; + margin-right: 10px; + object-fit: cover; + flex-shrink: 0; +} + +.verifier-suggestion-text { + display: flex; + flex-direction: column; + overflow: hidden; /* Prevent long text overflow */ + white-space: nowrap; +} + +.verifier-suggestion-name { + font-weight: bold; + text-overflow: ellipsis; + overflow: hidden; +} + +.verifier-suggestion-handle { + font-size: 0.9em; + color: var(--text-muted); + text-overflow: ellipsis; + overflow: hidden; +} + +/* Adjust handle color on hover */ +.verifier-suggestion-item:hover .verifier-suggestion-handle { + color: var(--button-text); /* Match parent hover color */ +} \ No newline at end of file diff --git a/src/components/Verifier/Verifier.js b/src/components/Verifier/Verifier.js index de87b1f..8b1028c 100644 --- a/src/components/Verifier/Verifier.js +++ b/src/components/Verifier/Verifier.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useAuth } from '../../contexts/AuthContext'; import { Agent } from '@atproto/api'; import './Verifier.css'; @@ -134,6 +134,11 @@ function Verifier() { const [isCheckingValidity, setIsCheckingValidity] = useState(false); const [networkStatusMessage, setNetworkStatusMessage] = useState(''); const [officialVerifiersStatus, setOfficialVerifiersStatus] = useState({}); + const [suggestions, setSuggestions] = useState([]); // State for typeahead suggestions + const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); // State for suggestion loading indicator + const [showSuggestions, setShowSuggestions] = useState(false); // Control suggestion list visibility + const debounceTimeoutRef = useRef(null); // Ref for debounce timer + const suggestionListRef = useRef(null); // Ref for suggestion list to handle clicks outside useEffect(() => { if (session) { @@ -565,11 +570,89 @@ function Verifier() { } }; + // Debounced function to fetch typeahead suggestions + const fetchSuggestions = useCallback(async (query) => { + if (!query || query.length < 1) { // Minimum query length + setSuggestions([]); + setShowSuggestions(false); + return; + } + setIsLoadingSuggestions(true); + setShowSuggestions(true); // Show list when fetching starts + try { + const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=10`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`API Error: ${response.status}`); + } + const data = await response.json(); + setSuggestions(data.actors || []); + } catch (error) { + console.error("Failed to fetch suggestions:", error); + setSuggestions([]); // Clear suggestions on error + } finally { + setIsLoadingSuggestions(false); + } + }, []); + + // Handler for input change with debouncing + const handleInputChange = (e) => { + const newHandle = e.target.value; + setTargetHandle(newHandle); + + // Clear existing debounce timer + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + if (newHandle.trim() === '') { + setSuggestions([]); + setShowSuggestions(false); + setIsLoadingSuggestions(false); + return; // Don't fetch if input is empty + } + + // Set new debounce timer + debounceTimeoutRef.current = setTimeout(() => { + fetchSuggestions(newHandle); + }, 300); // 300ms debounce delay + }; + + // Handler for clicking a suggestion + const handleSuggestionClick = (handle) => { + setTargetHandle(handle); + setSuggestions([]); + setShowSuggestions(false); + }; + + // Handler to hide suggestions when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if (suggestionListRef.current && !suggestionListRef.current.contains(event.target)) { + // Check if the click target is the input field itself to avoid immediate closing + if (!event.target.classList.contains('verifier-input-field')) { + setShowSuggestions(false); + } + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + // Handler for input focus to potentially show suggestions again if needed + const handleInputFocus = () => { + if (targetHandle.trim() !== '' && suggestions.length > 0) { + setShowSuggestions(true); + } + }; + // Handle loading and error states if (isAuthLoading) return
Loading authentication...
; if (authError) returnAuthentication Error: {authError}. Please login.
; - const isAnyOperationInProgress = isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity; + const isAnyOperationInProgress = isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity || isLoadingSuggestions; return (Enter the handle of the user you want to verify:
+