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