perf: Optimize profile data fetching by preventing re-showing loading spinner and unnecessary re-fetches on subsequent session changes.

This commit is contained in:
pradeepkumar
2026-03-04 21:40:16 +05:30
parent be863d4ec2
commit 27d3a24605
2 changed files with 331 additions and 158 deletions

View File

@@ -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,11 +26,17 @@ function ProfessionalCard({
experience,
expertise,
imageUrl,
isVerified,
rating,
profileLink,
}: ProfessionalCardProps) {
const starCount = rating ? Math.round(rating) : 0;
return (
<div className="bg-white rounded-[15px] overflow-hidden shadow-[0px_4px_20px_rgba(0,0,0,0.08)]">
<Link href={profileLink}>
<div className="bg-white rounded-[15px] overflow-hidden shadow-[0px_4px_20px_rgba(0,0,0,0.08)] hover:shadow-[0px_6px_24px_rgba(0,0,0,0.12)] transition-shadow cursor-pointer">
{/* Image */}
<div className="relative h-[226px] w-full overflow-hidden">
<div className="relative h-[226px] w-full overflow-hidden bg-gray-100">
<Image
src={imageUrl}
alt={name}
@@ -108,6 +58,7 @@ function ProfessionalCard({
</p>
{/* Verified Badge */}
{isVerified && (
<div className="flex items-center gap-1.5 mt-2">
<Image
src="/assets/icons/verified-badge-blue.svg"
@@ -119,8 +70,10 @@ function ProfessionalCard({
&ldquo;Verified local agent&rdquo;
</span>
</div>
)}
{/* Location */}
{location && (
<div className="mt-3">
<p className="font-fractul font-bold text-[14px] leading-normal text-[#00293d]">
Location:
@@ -137,8 +90,10 @@ function ProfessionalCard({
</span>
</div>
</div>
)}
{/* Expertise */}
{expertise.length > 0 && (
<div className="mt-3">
<p className="font-fractul font-bold text-[14px] leading-normal text-[#00293d]">
Expertise:
@@ -154,42 +109,222 @@ function ProfessionalCard({
))}
</div>
</div>
)}
{/* Experience */}
{experience && (
<p className="mt-3 text-[14px] leading-normal text-[#00293d]">
<span className="font-fractul font-bold">Experience:</span>
<span className="font-serif font-normal"> {experience}</span>
</p>
)}
{/* Star Rating */}
{starCount > 0 && (
<div className="flex gap-1 mt-3">
{[...Array(5)].map((_, i) => (
<Image
key={i}
src="/assets/icons/star-yellow.svg"
src={i < starCount ? '/assets/icons/star-yellow.svg' : '/assets/icons/star-empty-icon.svg'}
alt="Star"
width={18}
height={17}
/>
))}
</div>
)}
</div>
</div>
</Link>
);
}
// 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<string> {
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<DisplayProfessional[]>([]);
const [lenders, setLenders] = useState<DisplayProfessional[]>([]);
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<DisplayProfessional[]> => {
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 (
<section className="py-10 md:py-12 bg-white">
@@ -247,18 +382,48 @@ export function TopProfessionals({ content }: { content?: TopProfessionalsConten
{/* Cards Grid - 3 cards + CTA sidebar */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Professional Cards */}
{displayData.map((professional, index) => (
{loading ? (
// Loading skeleton cards
<>
{[1, 2, 3].map((i) => (
<div key={i} className="bg-white rounded-[15px] overflow-hidden shadow-[0px_4px_20px_rgba(0,0,0,0.08)] animate-pulse">
<div className="h-[226px] bg-gray-200" />
<div className="p-4 space-y-3">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-3 bg-gray-200 rounded w-1/2" />
<div className="h-4 bg-gray-200 rounded w-2/3" />
<div className="flex gap-1.5">
<div className="h-6 bg-gray-200 rounded-full w-16" />
<div className="h-6 bg-gray-200 rounded-full w-16" />
<div className="h-6 bg-gray-200 rounded-full w-16" />
</div>
</div>
</div>
))}
</>
) : displayData.length > 0 ? (
displayData.map((professional) => (
<ProfessionalCard
key={index}
key={professional.id}
name={professional.name}
subtitle={professional.subtitle}
location={professional.location}
experience={professional.experience}
expertise={professional.expertise}
imageUrl={professional.imageUrl}
isVerified={professional.isVerified}
rating={professional.rating}
profileLink={`/user/profile/${professional.slug}`}
/>
))}
))
) : (
// No data message
<div className="col-span-3 flex items-center justify-center py-12">
<p className="font-serif text-[14px] text-[#00293d]/60">
No {activeTab === 'agents' ? 'agents' : 'lenders'} available yet.
</p>
</div>
)}
{/* CTA Sidebar Card */}
<div

View File

@@ -54,12 +54,17 @@ export function ProfileSettingsForm({
const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Store original data for cancel/reset functionality
const [originalData, setOriginalData] = useState<typeof formData | null>(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 {
// 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]);