refactor: update TopProfessionals to use CMS data and implement dynamic featured agent fetching in FeaturesSection

This commit is contained in:
pradeepkumar
2026-03-28 22:04:20 +05:30
parent a60776be8b
commit 8dbc5c3842
3 changed files with 97 additions and 150 deletions

View File

@@ -4,6 +4,8 @@ import { useState, useEffect } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import type { FeaturesContent, FeaturedAgentItem } from '@/types/cms'; import type { FeaturesContent, FeaturedAgentItem } from '@/types/cms';
import { resolveImageUrl } from '@/services/cms.service'; import { resolveImageUrl } from '@/services/cms.service';
import { agentsService } from '@/services/agents.service';
import { uploadService } from '@/services/upload.service';
const defaultContent: FeaturesContent = { const defaultContent: FeaturesContent = {
title: 'Find Trusted Real Estate Professionals On Demand', title: 'Find Trusted Real Estate Professionals On Demand',
@@ -101,9 +103,49 @@ function AgentCard({ agent, className = '' }: { agent: FeaturedAgentItem; classN
export function FeaturesSection({ content }: { content?: FeaturesContent }) { export function FeaturesSection({ content }: { content?: FeaturesContent }) {
const data = content ?? defaultContent; const data = content ?? defaultContent;
const agents = data.featuredAgents?.length ? data.featuredAgents : defaultContent.featuredAgents!; const [agents, setAgents] = useState<FeaturedAgentItem[]>([]);
const [activeCard, setActiveCard] = useState(0); const [activeCard, setActiveCard] = useState(0);
useEffect(() => {
const fetchFeaturedAgents = async () => {
try {
const featured = await agentsService.getFeaturedAgents(3);
if (featured.length > 0) {
// Map API response to FeaturedAgentItem format
const mapped = await Promise.all(
featured.map(async (agent) => {
let imageUrl = '/assets/icons/user-placeholder-icon.svg';
if (agent.avatar) {
if (agent.avatar.startsWith('http') || agent.avatar.startsWith('/')) {
imageUrl = agent.avatar;
} else {
try {
imageUrl = await uploadService.getPresignedDownloadUrl(agent.avatar);
} catch (_e) {
// use placeholder
}
}
}
return {
name: `${agent.firstName || ''} ${agent.lastName || ''}`.trim(),
role: agent.agentType?.name || '',
rating: agent.averageRating ? String(agent.averageRating) : '',
location: agent.city || agent.state || '',
experience: agent.experience ? `${agent.experience}+ years in the real estate industry.` : '',
imageUrl,
};
}),
);
setAgents(mapped);
}
} catch (err) {
console.error('Failed to fetch featured agents:', err);
}
};
fetchFeaturedAgents();
}, []);
return ( return (
<section className="py-16 md:py-20 bg-white"> <section className="py-16 md:py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -128,32 +170,36 @@ export function FeaturesSection({ content }: { content?: FeaturesContent }) {
</div> </div>
{/* Right Side - Agent Cards (Desktop) */} {/* Right Side - Agent Cards (Desktop) */}
<div className="lg:w-1/2 relative min-h-[650px] hidden lg:block"> {agents.length > 0 && (
{agents[0] && <AgentCard agent={agents[0]} className="absolute top-0 left-0 shadow-lg z-10" />} <div className="lg:w-1/2 relative min-h-[650px] hidden lg:block">
{agents[1] && <AgentCard agent={agents[1]} className="absolute top-[170px] right-0 shadow-lg z-20" />} {agents[0] && <AgentCard agent={agents[0]} className="absolute top-0 left-0 shadow-lg z-10" />}
{agents[2] && <AgentCard agent={agents[2]} className="absolute top-[320px] left-[60px] shadow-lg z-30" />} {agents[1] && <AgentCard agent={agents[1]} className="absolute top-[170px] right-0 shadow-lg z-20" />}
</div> {agents[2] && <AgentCard agent={agents[2]} className="absolute top-[320px] left-[60px] shadow-lg z-30" />}
</div>
)}
{/* Mobile Agent Cards - Tab style with dots */} {/* Mobile Agent Cards - Tab style with dots */}
<div className="lg:hidden"> {agents.length > 0 && (
<div className="flex justify-center"> <div className="lg:hidden">
<AgentCard agent={agents[activeCard]} className="shadow-lg" /> <div className="flex justify-center">
<AgentCard agent={agents[activeCard]} className="shadow-lg" />
</div>
{/* Dot indicators */}
<div className="flex justify-center gap-2 mt-4">
{agents.map((_, index) => (
<button
key={index}
onClick={() => setActiveCard(index)}
className={`rounded-full transition-all ${
index === activeCard
? 'w-[10px] h-[10px] bg-[#00293d]'
: 'w-[8px] h-[8px] bg-[#00293d]/30'
}`}
/>
))}
</div>
</div> </div>
{/* Dot indicators */} )}
<div className="flex justify-center gap-2 mt-4">
{agents.map((_, index) => (
<button
key={index}
onClick={() => setActiveCard(index)}
className={`rounded-full transition-all ${
index === activeCard
? 'w-[10px] h-[10px] bg-[#00293d]'
: 'w-[8px] h-[8px] bg-[#00293d]/30'
}`}
/>
))}
</div>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -3,7 +3,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { agentsService, PublicAgentProfile, AgentType } from '@/services/agents.service';
import { uploadService } from '@/services/upload.service'; import { uploadService } from '@/services/upload.service';
import type { TopProfessionalsContent } from '@/types/cms'; import type { TopProfessionalsContent } from '@/types/cms';
@@ -157,80 +156,6 @@ function ProfessionalCard({
); );
} }
// 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 // Helper to resolve avatar URL
async function resolveAvatarUrl(avatar: string | null): Promise<string> { async function resolveAvatarUrl(avatar: string | null): Promise<string> {
if (!avatar) return '/assets/icons/user-placeholder-icon.svg'; if (!avatar) return '/assets/icons/user-placeholder-icon.svg';
@@ -271,76 +196,47 @@ export function TopProfessionals({ content }: { content?: TopProfessionalsConten
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchProfessionals = async () => { const loadProfessionals = async () => {
try { try {
// Get agent types to find Professional and Lender type IDs // Map CMS data to display format, resolving avatar URLs
const agentTypes = await agentsService.getAgentTypes(); const mapCmsEntries = async (
const professionalType = agentTypes.find(t => t.name === 'Professional'); entries: TopProfessionalsContent['agents'],
const lenderType = agentTypes.find(t => t.name === 'Lender'); ): Promise<DisplayProfessional[]> => {
// 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( return Promise.all(
profiles.map(async (profile) => { entries.map(async (entry, index) => {
const imageUrl = await resolveAvatarUrl(profile.avatar); const imageUrl = await resolveAvatarUrl(entry.imageUrl || null);
const expertise = getExpertiseTags(profile);
const location = getLocationString(profile);
const experienceYears = profile.experience
? `${profile.experience}+ years in real estate.`
: '';
return { return {
id: profile.id, id: `cms-${index}`,
name: `${profile.firstName} ${profile.lastName}`.trim(), name: entry.name,
subtitle: profile.agentType ? `(${profile.agentType.name})` : '', subtitle: entry.subtitle || '',
location, location: entry.location || '',
experience: experienceYears, experience: entry.experience || '',
expertise, expertise: entry.expertise || [],
imageUrl, imageUrl,
isVerified: profile.isVerified, isVerified: false,
rating: profile.rating, rating: null,
slug: profile.slug, slug: '',
}; };
}) }),
); );
}; };
const [mappedAgents, mappedLenders] = await Promise.all([ const [mappedAgents, mappedLenders] = await Promise.all([
mapProfiles(agentsResponse.data), mapCmsEntries(data.agents || []),
mapProfiles(lendersResponse.data), mapCmsEntries(data.lenders || []),
]); ]);
setAgents(mappedAgents); setAgents(mappedAgents);
setLenders(mappedLenders); setLenders(mappedLenders);
} catch (err) { } catch (err) {
console.error('Failed to fetch top professionals:', err); console.error('Failed to load top professionals:', err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
fetchProfessionals(); loadProfessionals();
}, []); }, [data.agents, data.lenders]);
const displayData = activeTab === 'agents' ? agents : lenders; const displayData = activeTab === 'agents' ? agents : lenders;

View File

@@ -191,6 +191,11 @@ class AgentsService {
return response.data.data; return response.data.data;
} }
async getFeaturedAgents(limit = 3): Promise<PublicAgentProfile[]> {
const response = await api.get<ApiResponse<PublicAgentProfile[]>>(`/agents/featured?limit=${limit}`);
return response.data.data;
}
async searchAgents(params: SearchAgentsParams = {}): Promise<SearchAgentsResponse> { async searchAgents(params: SearchAgentsParams = {}): Promise<SearchAgentsResponse> {
const queryParams = new URLSearchParams(); const queryParams = new URLSearchParams();