From 27d3a24605293a22841d9bb3ae3090628e8627b8 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Wed, 4 Mar 2026 21:40:16 +0530 Subject: [PATCH] perf: Optimize profile data fetching by preventing re-showing loading spinner and unnecessary re-fetches on subsequent session changes. --- src/components/home/TopProfessionals.tsx | 479 ++++++++++++------ .../settings/ProfileSettingsForm.tsx | 10 +- 2 files changed, 331 insertions(+), 158 deletions(-) diff --git a/src/components/home/TopProfessionals.tsx b/src/components/home/TopProfessionals.tsx index 3c36f36..4615d35 100644 --- a/src/components/home/TopProfessionals.tsx +++ b/src/components/home/TopProfessionals.tsx @@ -1,71 +1,12 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Link from 'next/link'; import Image from 'next/image'; +import { agentsService, PublicAgentProfile, AgentType } from '@/services/agents.service'; +import { uploadService } from '@/services/upload.service'; import type { TopProfessionalsContent } from '@/types/cms'; -// Sample agents data for tabs -const agentsData = [ - { - id: '1', - name: 'Arjun Mehta', - subtitle: '(Residential Property Expert)', - location: 'San Francisco, CA', - experience: '10+ years in the real estate industry.', - expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], - imageUrl: '/assets/images/professional-1.jpg', - }, - { - id: '2', - name: 'Arjun Mehta', - subtitle: '(Residential Property Expert)', - location: 'San Francisco, CA', - experience: '10+ years in the real estate industry.', - expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], - imageUrl: '/assets/images/professional-2.jpg', - }, - { - id: '3', - name: 'Arjun Mehta', - subtitle: '(Residential Property Expert)', - location: 'San Francisco, CA', - experience: '10+ years in the real estate industry.', - expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], - imageUrl: '/assets/images/professional-3.jpg', - }, -]; - -const lendersData = [ - { - id: '4', - name: 'Sarah Johnson', - subtitle: '(Mortgage Specialist)', - location: 'Los Angeles, CA', - experience: '10+ years in mortgage lending.', - expertise: ['FHA Loans', 'Conventional', 'VA Loans', 'Refinancing', 'Jumbo'], - imageUrl: '/assets/images/professional-1.jpg', - }, - { - id: '5', - name: 'Michael Chen', - subtitle: '(Senior Loan Officer)', - location: 'Seattle, WA', - experience: '7+ years in lending.', - expertise: ['Jumbo Loans', 'Refinancing', 'Investment', 'First-time', 'USDA'], - imageUrl: '/assets/images/professional-2.jpg', - }, - { - id: '6', - name: 'Emily Davis', - subtitle: '(Home Loan Advisor)', - location: 'Austin, TX', - experience: '5+ years in mortgage industry.', - expertise: ['First-time Buyers', 'FHA', 'USDA Loans', 'VA Loans', 'Conventional'], - imageUrl: '/assets/images/professional-3.jpg', - }, -]; - interface ProfessionalCardProps { name: string; subtitle: string; @@ -73,6 +14,9 @@ interface ProfessionalCardProps { experience: string; expertise: string[]; imageUrl: string; + isVerified: boolean; + rating: number | null; + profileLink: string; } function ProfessionalCard({ @@ -82,114 +26,305 @@ function ProfessionalCard({ experience, expertise, imageUrl, + isVerified, + rating, + profileLink, }: ProfessionalCardProps) { + const starCount = rating ? Math.round(rating) : 0; + return ( -
- {/* Image */} -
- {name} -
- - {/* Content */} -
- {/* Name */} -

- {name} -

- - {/* Subtitle */} -

- {subtitle} -

- - {/* Verified Badge */} -
+ +
+ {/* Image */} +
Verified - - “Verified local agent” -
- {/* Location */} -
-

- Location: -

-
- Location - - {location} - -
-
+ {/* Content */} +
+ {/* Name */} +

+ {name} +

- {/* Expertise */} -
-

- Expertise: + {/* Subtitle */} +

+ {subtitle}

-
- {expertise.slice(0, 6).map((tag, index) => ( - - {tag} + + {/* Verified Badge */} + {isVerified && ( +
+ Verified + + “Verified local agent” - ))} -
-
+
+ )} - {/* Experience */} -

- Experience: - {experience} -

+ {/* Location */} + {location && ( +
+

+ Location: +

+
+ Location + + {location} + +
+
+ )} - {/* Star Rating */} -
- {[...Array(5)].map((_, i) => ( - Star - ))} + {/* Expertise */} + {expertise.length > 0 && ( +
+

+ Expertise: +

+
+ {expertise.slice(0, 6).map((tag, index) => ( + + {tag} + + ))} +
+
+ )} + + {/* Experience */} + {experience && ( +

+ Experience: + {experience} +

+ )} + + {/* Star Rating */} + {starCount > 0 && ( +
+ {[...Array(5)].map((_, i) => ( + Star + ))} +
+ )}
-
+ ); } +// Helper to extract expertise tags from agent profile +function getExpertiseTags(profile: PublicAgentProfile): string[] { + const tags: string[] = []; + const expertiseFieldSlugs = [ + 'about_me_expertise', + 'expertise_areas', + 'specializations', + 'areas_of_expertise', + 'expertise', + ]; + + if (profile.fieldValues && profile.fieldValues.length > 0) { + profile.fieldValues.forEach((fv) => { + if (!expertiseFieldSlugs.includes(fv.field.slug)) return; + const value = fv.jsonValue; + if (Array.isArray(value)) { + value.forEach((v) => { + if (typeof v === 'string' && v.trim()) { + const displayValue = v + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + if (!tags.includes(displayValue)) tags.push(displayValue); + } + }); + } else if (typeof value === 'string' && value.trim()) { + if (!tags.includes(value)) tags.push(value); + } + }); + } + + if (profile.specializations && profile.specializations.length > 0) { + profile.specializations.forEach((spec) => { + if (!tags.includes(spec)) tags.push(spec); + }); + } + + return tags; +} + +// Helper to get location string from agent profile +function getLocationString(profile: PublicAgentProfile): string { + if (profile.fieldValues && profile.fieldValues.length > 0) { + const stateField = profile.fieldValues.find(fv => fv.field.slug === 'state'); + const cityField = profile.fieldValues.find(fv => fv.field.slug === 'city'); + const parts: string[] = []; + + if (cityField) { + const val = cityField.jsonValue; + if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') { + parts.push(val[0].split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ')); + } else if (typeof val === 'string') { + parts.push(val); + } + } + if (stateField) { + const val = stateField.jsonValue; + if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') { + parts.push(val[0].split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ')); + } else if (typeof val === 'string') { + parts.push(val); + } + } + if (parts.length > 0) return parts.join(', '); + } + + const legacyParts = [profile.city, profile.state].filter(Boolean); + if (legacyParts.length > 0) return legacyParts.join(', '); + + if (profile.serviceAreas && profile.serviceAreas.length > 0) return profile.serviceAreas[0]; + + return ''; +} + +// Helper to resolve avatar URL +async function resolveAvatarUrl(avatar: string | null): Promise { + if (!avatar) return '/assets/icons/user-placeholder-icon.svg'; + if (avatar.startsWith('http') || avatar.startsWith('/')) return avatar; + try { + return await uploadService.getPresignedDownloadUrl(avatar); + } catch { + return '/assets/icons/user-placeholder-icon.svg'; + } +} + +interface DisplayProfessional { + id: string; + name: string; + subtitle: string; + location: string; + experience: string; + expertise: string[]; + imageUrl: string; + isVerified: boolean; + rating: number | null; + slug: string; +} + const defaultContent: TopProfessionalsContent = { title: '\u201cMeet top real estate professionals\u201d', ctaText: 'Discover 5,000+ Top Real Estate Agents in Our Network.', ctaButtonText: 'Browse Experts', - agents: agentsData, - lenders: lendersData, + agents: [], + lenders: [], }; export function TopProfessionals({ content }: { content?: TopProfessionalsContent }) { const data = content ?? defaultContent; const [activeTab, setActiveTab] = useState<'agents' | 'lenders'>('agents'); - const displayData = activeTab === 'agents' ? data.agents : data.lenders; + const [agents, setAgents] = useState([]); + const [lenders, setLenders] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchProfessionals = async () => { + try { + // Get agent types to find Professional and Lender type IDs + const agentTypes = await agentsService.getAgentTypes(); + const professionalType = agentTypes.find(t => t.name === 'Professional'); + const lenderType = agentTypes.find(t => t.name === 'Lender'); + + // Fetch both in parallel - get top 3 of each sorted by rating + const [agentsResponse, lendersResponse] = await Promise.all([ + professionalType + ? agentsService.searchAgents({ + agentTypeId: professionalType.id, + limit: 3, + sortBy: 'averageRating', + sortOrder: 'desc', + }) + : Promise.resolve({ data: [], total: 0, page: 1, limit: 3, totalPages: 0 }), + lenderType + ? agentsService.searchAgents({ + agentTypeId: lenderType.id, + limit: 3, + sortBy: 'averageRating', + sortOrder: 'desc', + }) + : Promise.resolve({ data: [], total: 0, page: 1, limit: 3, totalPages: 0 }), + ]); + + // Resolve avatars and map to display format + const mapProfiles = async (profiles: PublicAgentProfile[]): Promise => { + return Promise.all( + profiles.map(async (profile) => { + const imageUrl = await resolveAvatarUrl(profile.avatar); + const expertise = getExpertiseTags(profile); + const location = getLocationString(profile); + const experienceYears = profile.experience + ? `${profile.experience}+ years in real estate.` + : ''; + + return { + id: profile.id, + name: `${profile.firstName} ${profile.lastName}`.trim(), + subtitle: profile.agentType ? `(${profile.agentType.name})` : '', + location, + experience: experienceYears, + expertise, + imageUrl, + isVerified: profile.isVerified, + rating: profile.rating, + slug: profile.slug, + }; + }) + ); + }; + + const [mappedAgents, mappedLenders] = await Promise.all([ + mapProfiles(agentsResponse.data), + mapProfiles(lendersResponse.data), + ]); + + setAgents(mappedAgents); + setLenders(mappedLenders); + } catch (err) { + console.error('Failed to fetch top professionals:', err); + } finally { + setLoading(false); + } + }; + + fetchProfessionals(); + }, []); + + const displayData = activeTab === 'agents' ? agents : lenders; return (
@@ -247,18 +382,48 @@ export function TopProfessionals({ content }: { content?: TopProfessionalsConten {/* Cards Grid - 3 cards + CTA sidebar */}
- {/* Professional Cards */} - {displayData.map((professional, index) => ( - - ))} + {loading ? ( + // Loading skeleton cards + <> + {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} + + ) : displayData.length > 0 ? ( + displayData.map((professional) => ( + + )) + ) : ( + // No data message +
+

+ No {activeTab === 'agents' ? 'agents' : 'lenders'} available yet. +

+
+ )} {/* CTA Sidebar Card */}
(null); // Store original data for cancel/reset functionality const [originalData, setOriginalData] = useState(null); + // Track if initial profile fetch is done to avoid re-showing loading spinner + const hasFetchedRef = useRef(false); // Fetch profile data on mount based on user role useEffect(() => { const fetchProfile = async () => { try { - setIsLoading(true); + // Only show loading spinner on initial fetch, not on session-triggered re-fetches + if (!hasFetchedRef.current) { + setIsLoading(true); + } const role = (session?.user as any)?.role; if (role === 'AGENT') { @@ -122,10 +127,13 @@ export function ProfileSettingsForm({ } } finally { setIsLoading(false); + hasFetchedRef.current = true; } }; if (session) { + // Skip re-fetching on session changes after initial load (e.g. after updateSession in handleSave) + if (hasFetchedRef.current) return; fetchProfile(); } }, [session]);