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 */}
-
-
-
-
- {/* Content */}
-
- {/* Name */}
-
- {name}
-
-
- {/* Subtitle */}
-
- {subtitle}
-
-
- {/* Verified Badge */}
-
+
+
+ {/* Image */}
+
-
- “Verified local agent”
-
- {/* Location */}
-
-
- Location:
-
-
-
-
- {location}
-
-
-
+ {/* Content */}
+
+ {/* Name */}
+
+ {name}
+
- {/* Expertise */}
-
-
- Expertise:
+ {/* Subtitle */}
+
+ {subtitle}
-
- {expertise.slice(0, 6).map((tag, index) => (
-
- {tag}
+
+ {/* Verified Badge */}
+ {isVerified && (
+
+
+
+ “Verified local agent”
- ))}
-
-
+
+ )}
- {/* Experience */}
-
- Experience:
- {experience}
-
+ {/* Location */}
+ {location && (
+
+
+ Location:
+
+
+
+
+ {location}
+
+
+
+ )}
- {/* Star Rating */}
-
- {[...Array(5)].map((_, i) => (
-
- ))}
+ {/* 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) => (
+
+ ))}
+
+ )}
-
+
);
}
+// 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]);