feat: Optimize agent profile avatar loading by resolving all presigned URLs in parallel at the page level and passing them to individual profile cards.
This commit is contained in:
@@ -58,41 +58,16 @@ function FilterSection({ title, options, selectedOptions, onToggle, showMore }:
|
||||
|
||||
interface ProfileCardProps {
|
||||
profile: PublicAgentProfile;
|
||||
resolvedAvatarUrl?: string | null;
|
||||
}
|
||||
|
||||
function ProfileCard({ profile }: ProfileCardProps) {
|
||||
function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
||||
const [showFullBio, setShowFullBio] = useState(false);
|
||||
const [showAllTags, setShowAllTags] = useState(false);
|
||||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
||||
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
|
||||
|
||||
|
||||
// Fetch presigned URL for avatar if it's an S3 key
|
||||
useEffect(() => {
|
||||
const fetchAvatarUrl = async () => {
|
||||
if (profile.avatar && !profile.avatar.startsWith('http') && !profile.avatar.startsWith('/')) {
|
||||
try {
|
||||
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
||||
// Add cache-busting parameter to force browser to fetch fresh image
|
||||
// This prevents showing old cached image when avatar is updated
|
||||
const profileWithTimestamp = profile as unknown as { updatedAt?: string };
|
||||
const cacheBuster = profileWithTimestamp.updatedAt
|
||||
? new Date(profileWithTimestamp.updatedAt).getTime()
|
||||
: Date.now();
|
||||
const urlWithCacheBuster = `${presignedUrl}&_t=${cacheBuster}`;
|
||||
setAvatarUrl(urlWithCacheBuster);
|
||||
} catch (error) {
|
||||
console.error('Failed to get avatar URL:', error);
|
||||
}
|
||||
} else if (profile.avatar) {
|
||||
setAvatarUrl(profile.avatar);
|
||||
}
|
||||
};
|
||||
fetchAvatarUrl();
|
||||
}, [profile.avatar, profile]);
|
||||
|
||||
const getProfileImageUrl = () => {
|
||||
if (avatarUrl) return avatarUrl;
|
||||
if (resolvedAvatarUrl) return resolvedAvatarUrl;
|
||||
if (profile.avatar?.startsWith('/')) return profile.avatar;
|
||||
return defaultImage;
|
||||
};
|
||||
@@ -405,6 +380,7 @@ function ProfilesPageContent() {
|
||||
|
||||
// State for API data
|
||||
const [agents, setAgents] = useState<PublicAgentProfile[]>([]);
|
||||
const [avatarUrls, setAvatarUrls] = useState<Record<string, string>>({});
|
||||
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
||||
const [filterFields, setFilterFields] = useState<FilterField[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -474,10 +450,9 @@ function ProfilesPageContent() {
|
||||
params.agentTypeId = activeTypeId;
|
||||
}
|
||||
|
||||
// Add location from URL params
|
||||
// Add location search (searches city, state, country, and dynamic field values)
|
||||
if (locationFromUrl) {
|
||||
// Try to set as city first
|
||||
params.city = locationFromUrl;
|
||||
params.location = locationFromUrl;
|
||||
}
|
||||
|
||||
// Add dynamic profile field filters
|
||||
@@ -493,6 +468,28 @@ function ProfilesPageContent() {
|
||||
}
|
||||
|
||||
const response = await agentsService.searchAgents(params);
|
||||
|
||||
// Resolve all avatar presigned URLs in parallel before rendering
|
||||
const urlMap: Record<string, string> = {};
|
||||
const avatarPromises = response.data.map(async (agent) => {
|
||||
if (agent.avatar && !agent.avatar.startsWith('http') && !agent.avatar.startsWith('/')) {
|
||||
try {
|
||||
const presignedUrl = await uploadService.getPresignedDownloadUrl(agent.avatar);
|
||||
const agentWithTimestamp = agent as unknown as { updatedAt?: string };
|
||||
const cacheBuster = agentWithTimestamp.updatedAt
|
||||
? new Date(agentWithTimestamp.updatedAt).getTime()
|
||||
: Date.now();
|
||||
urlMap[agent.id] = `${presignedUrl}&_t=${cacheBuster}`;
|
||||
} catch {
|
||||
// Skip failed URLs, card will show default image
|
||||
}
|
||||
} else if (agent.avatar) {
|
||||
urlMap[agent.id] = agent.avatar;
|
||||
}
|
||||
});
|
||||
await Promise.all(avatarPromises);
|
||||
|
||||
setAvatarUrls(urlMap);
|
||||
setAgents(response.data);
|
||||
setTotalPages(response.totalPages);
|
||||
setTotalResults(response.total);
|
||||
@@ -703,7 +700,7 @@ function ProfilesPageContent() {
|
||||
{!loading && !error && agents.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{agents.map((profile) => (
|
||||
<ProfileCard key={profile.id} profile={profile} />
|
||||
<ProfileCard key={profile.id} profile={profile} resolvedAvatarUrl={avatarUrls[profile.id]} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface SaveFieldValuesResponse {
|
||||
// Search parameters for public agent search
|
||||
export interface SearchAgentsParams {
|
||||
search?: string;
|
||||
location?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
@@ -187,6 +188,7 @@ class AgentsService {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params.search) queryParams.set('search', params.search);
|
||||
if (params.location) queryParams.set('location', params.location);
|
||||
if (params.city) queryParams.set('city', params.city);
|
||||
if (params.state) queryParams.set('state', params.state);
|
||||
if (params.country) queryParams.set('country', params.country);
|
||||
|
||||
Reference in New Issue
Block a user