This repository has no description
0

Configure Feed

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

add autocomplete

+205 -16
+84 -1
src/components/Verifier/Verifier.css
··· 380 380 .verifier-verified-status { color: var(--success-text, green); } 381 381 .verifier-not-verified-status { color: var(--text-muted, grey); } 382 382 .verifier-error-status { color: var(--error-text, red); } 383 - .verifier-checking-status, .verifier-idle-status { color: var(--text-muted, grey); } 383 + .verifier-checking-status, .verifier-idle-status { color: var(--text-muted, grey); } 384 + 385 + /* Styles for Typeahead/Autocomplete Suggestions */ 386 + .verifier-input-wrapper { 387 + position: relative; /* Required for absolute positioning of suggestions */ 388 + } 389 + 390 + .verifier-suggestions-list { 391 + list-style: none; 392 + padding: 0; 393 + margin: 5px 0 0 0; 394 + position: absolute; 395 + top: 100%; /* Position below the input */ 396 + left: 0; 397 + right: 0; 398 + background-color: var(--navbar-bg); /* Match nearby elements */ 399 + border: 1px solid var(--card-border); 400 + border-top: none; /* Avoid double border with input */ 401 + border-radius: 0 0 6px 6px; /* Round bottom corners */ 402 + max-height: 250px; /* Limit height and allow scroll */ 403 + overflow-y: auto; 404 + z-index: 1000; /* Ensure it appears above other content */ 405 + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 406 + } 407 + 408 + .verifier-suggestion-item { 409 + display: flex; 410 + align-items: center; 411 + padding: 10px 15px; 412 + cursor: pointer; 413 + transition: background-color 0.2s ease; 414 + } 415 + 416 + .verifier-suggestion-item:hover { 417 + background-color: var(--button-hover-bg); /* Use button hover for consistency */ 418 + color: var(--button-text); 419 + } 420 + 421 + .verifier-suggestion-item.loading, 422 + .verifier-suggestion-item.none { 423 + font-style: italic; 424 + color: var(--text-muted); 425 + cursor: default; 426 + } 427 + 428 + .verifier-suggestion-item:hover.loading, 429 + .verifier-suggestion-item:hover.none { 430 + background-color: transparent; /* Don't highlight loading/none items */ 431 + color: var(--text-muted); 432 + } 433 + 434 + .verifier-suggestion-avatar { 435 + width: 30px; 436 + height: 30px; 437 + border-radius: 50%; 438 + margin-right: 10px; 439 + object-fit: cover; 440 + flex-shrink: 0; 441 + } 442 + 443 + .verifier-suggestion-text { 444 + display: flex; 445 + flex-direction: column; 446 + overflow: hidden; /* Prevent long text overflow */ 447 + white-space: nowrap; 448 + } 449 + 450 + .verifier-suggestion-name { 451 + font-weight: bold; 452 + text-overflow: ellipsis; 453 + overflow: hidden; 454 + } 455 + 456 + .verifier-suggestion-handle { 457 + font-size: 0.9em; 458 + color: var(--text-muted); 459 + text-overflow: ellipsis; 460 + overflow: hidden; 461 + } 462 + 463 + /* Adjust handle color on hover */ 464 + .verifier-suggestion-item:hover .verifier-suggestion-handle { 465 + color: var(--button-text); /* Match parent hover color */ 466 + }
+109 -4
src/components/Verifier/Verifier.js
··· 1 - import React, { useState, useEffect, useCallback } from 'react'; 1 + import React, { useState, useEffect, useCallback, useRef } from 'react'; 2 2 import { useAuth } from '../../contexts/AuthContext'; 3 3 import { Agent } from '@atproto/api'; 4 4 import './Verifier.css'; ··· 134 134 const [isCheckingValidity, setIsCheckingValidity] = useState(false); 135 135 const [networkStatusMessage, setNetworkStatusMessage] = useState(''); 136 136 const [officialVerifiersStatus, setOfficialVerifiersStatus] = useState({}); 137 + const [suggestions, setSuggestions] = useState([]); // State for typeahead suggestions 138 + const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); // State for suggestion loading indicator 139 + const [showSuggestions, setShowSuggestions] = useState(false); // Control suggestion list visibility 140 + const debounceTimeoutRef = useRef(null); // Ref for debounce timer 141 + const suggestionListRef = useRef(null); // Ref for suggestion list to handle clicks outside 137 142 138 143 useEffect(() => { 139 144 if (session) { ··· 565 570 } 566 571 }; 567 572 573 + // Debounced function to fetch typeahead suggestions 574 + const fetchSuggestions = useCallback(async (query) => { 575 + if (!query || query.length < 1) { // Minimum query length 576 + setSuggestions([]); 577 + setShowSuggestions(false); 578 + return; 579 + } 580 + setIsLoadingSuggestions(true); 581 + setShowSuggestions(true); // Show list when fetching starts 582 + try { 583 + const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=10`; 584 + const response = await fetch(url); 585 + if (!response.ok) { 586 + throw new Error(`API Error: ${response.status}`); 587 + } 588 + const data = await response.json(); 589 + setSuggestions(data.actors || []); 590 + } catch (error) { 591 + console.error("Failed to fetch suggestions:", error); 592 + setSuggestions([]); // Clear suggestions on error 593 + } finally { 594 + setIsLoadingSuggestions(false); 595 + } 596 + }, []); 597 + 598 + // Handler for input change with debouncing 599 + const handleInputChange = (e) => { 600 + const newHandle = e.target.value; 601 + setTargetHandle(newHandle); 602 + 603 + // Clear existing debounce timer 604 + if (debounceTimeoutRef.current) { 605 + clearTimeout(debounceTimeoutRef.current); 606 + } 607 + 608 + if (newHandle.trim() === '') { 609 + setSuggestions([]); 610 + setShowSuggestions(false); 611 + setIsLoadingSuggestions(false); 612 + return; // Don't fetch if input is empty 613 + } 614 + 615 + // Set new debounce timer 616 + debounceTimeoutRef.current = setTimeout(() => { 617 + fetchSuggestions(newHandle); 618 + }, 300); // 300ms debounce delay 619 + }; 620 + 621 + // Handler for clicking a suggestion 622 + const handleSuggestionClick = (handle) => { 623 + setTargetHandle(handle); 624 + setSuggestions([]); 625 + setShowSuggestions(false); 626 + }; 627 + 628 + // Handler to hide suggestions when clicking outside 629 + useEffect(() => { 630 + const handleClickOutside = (event) => { 631 + if (suggestionListRef.current && !suggestionListRef.current.contains(event.target)) { 632 + // Check if the click target is the input field itself to avoid immediate closing 633 + if (!event.target.classList.contains('verifier-input-field')) { 634 + setShowSuggestions(false); 635 + } 636 + } 637 + }; 638 + document.addEventListener('mousedown', handleClickOutside); 639 + return () => { 640 + document.removeEventListener('mousedown', handleClickOutside); 641 + }; 642 + }, []); 643 + 644 + // Handler for input focus to potentially show suggestions again if needed 645 + const handleInputFocus = () => { 646 + if (targetHandle.trim() !== '' && suggestions.length > 0) { 647 + setShowSuggestions(true); 648 + } 649 + }; 650 + 568 651 // Handle loading and error states 569 652 if (isAuthLoading) return <p>Loading authentication...</p>; 570 653 if (authError) return <p>Authentication Error: {authError}. <a href="/login">Please login</a>.</p>; 571 654 572 - const isAnyOperationInProgress = isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity; 655 + const isAnyOperationInProgress = isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity || isLoadingSuggestions; 573 656 574 657 return ( 575 658 <div className="verifier-container"> ··· 590 673 <div className="verifier-section"> 591 674 <h2>Verify a Bluesky User</h2> 592 675 <p>Enter the handle of the user you want to verify:</p> 676 + <div className="verifier-input-wrapper"> 593 677 <form onSubmit={handleVerify} className="verifier-form-container" style={{ marginBottom: 0 }}> 594 678 <input 595 679 type="text" 596 680 value={targetHandle} 597 - onChange={(e) => setTargetHandle(e.target.value)} 681 + onChange={handleInputChange} 682 + onFocus={handleInputFocus} 598 683 placeholder="username.bsky.social" 599 - disabled={isAnyOperationInProgress} 684 + disabled={isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity} 600 685 required 601 686 className="verifier-input-field" 602 687 autoComplete="off" ··· 605 690 {isVerifying ? 'Verifying...' : 'Verify Account'} 606 691 </button> 607 692 </form> 693 + {showSuggestions && ( 694 + <ul className="verifier-suggestions-list" ref={suggestionListRef}> 695 + {isLoadingSuggestions ? ( 696 + <li className="verifier-suggestion-item loading">Loading suggestions...</li> 697 + ) : suggestions.length > 0 ? ( 698 + suggestions.map(actor => ( 699 + <li key={actor.did} className="verifier-suggestion-item" onClick={() => handleSuggestionClick(actor.handle)}> 700 + <img src={actor.avatar} alt="" className="verifier-suggestion-avatar" onError={(e) => e.target.style.display = 'none'} /> 701 + <div className="verifier-suggestion-text"> 702 + <span className="verifier-suggestion-name">{actor.displayName || actor.handle}</span> 703 + <span className="verifier-suggestion-handle">@{actor.handle}</span> 704 + </div> 705 + </li> 706 + )) 707 + ) : ( 708 + <li className="verifier-suggestion-item none">No users found.</li> 709 + )} 710 + </ul> 711 + )} 712 + </div> 608 713 </div> 609 714 610 715 {statusMessage && (
+12 -11
src/contexts/AuthContext.js
··· 4 4 // Create auth context 5 5 export const AuthContext = createContext(null); 6 6 7 - // Set the appropriate domain based on the current hostname 8 - let domain = 'https://cred.blue'; 7 + // Determine domain from environment variable or default 8 + const domain = process.env.REACT_APP_CRED_BLUE_DOMAIN || 'https://cred.blue'; 9 + console.log(`(AuthProvider) Using domain: ${domain}`); // Log the domain being used 9 10 10 - // Always use the current domain for client_id to ensure it matches the host 11 - const metadataUrl = `https://cred.blue/client-metadata.json`; 11 + // Construct metadata URL based on the domain 12 + const metadataUrl = `${domain}/client-metadata.json`; 12 13 13 - // Client metadata for Bluesky OAuth 14 + // Client metadata for Bluesky OAuth - uses dynamic domain 14 15 const clientMetadata = { 15 - client_id: metadataUrl, 16 - client_name: "Cred.blue", 17 - client_uri: domain, 18 - redirect_uris: [`https://cred.blue/login/callback`], 19 - logo_uri: `https://cred.blue/favicon.ico`, 16 + client_id: metadataUrl, // Use dynamically generated URL 17 + client_name: "Cred.blue", // Keep name consistent or make dynamic if needed 18 + client_uri: domain, // Use dynamic domain 19 + redirect_uris: [`${domain}/login/callback`], // Use dynamic domain 20 + logo_uri: `${domain}/favicon.ico`, // Use dynamic domain 20 21 scope: "atproto transition:generic", 21 22 grant_types: ["authorization_code", "refresh_token"], 22 23 response_types: ["code"], ··· 44 45 try { 45 46 // Create the client instance 46 47 const oauthClient = new BrowserOAuthClient({ 47 - clientMetadata: clientMetadata, 48 + clientMetadata: clientMetadata, // Pass the dynamically configured metadata 48 49 handleResolver: 'https://public.api.bsky.app', 49 50 plcDirectoryUrl: 'https://plc.directory', 50 51 });