diff --git a/src/components/home/FeaturesSection.tsx b/src/components/home/FeaturesSection.tsx index 894c536..2e335bd 100644 --- a/src/components/home/FeaturesSection.tsx +++ b/src/components/home/FeaturesSection.tsx @@ -4,6 +4,8 @@ import { useState, useEffect } from 'react'; import Image from 'next/image'; import type { FeaturesContent, FeaturedAgentItem } from '@/types/cms'; import { resolveImageUrl } from '@/services/cms.service'; +import { agentsService } from '@/services/agents.service'; +import { uploadService } from '@/services/upload.service'; const defaultContent: FeaturesContent = { 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 }) { const data = content ?? defaultContent; - const agents = data.featuredAgents?.length ? data.featuredAgents : defaultContent.featuredAgents!; + const [agents, setAgents] = useState([]); 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 (
@@ -128,32 +170,36 @@ export function FeaturesSection({ content }: { content?: FeaturesContent }) {
{/* Right Side - Agent Cards (Desktop) */} -
- {agents[0] && } - {agents[1] && } - {agents[2] && } -
+ {agents.length > 0 && ( +
+ {agents[0] && } + {agents[1] && } + {agents[2] && } +
+ )} {/* Mobile Agent Cards - Tab style with dots */} -
-
- + {agents.length > 0 && ( +
+
+ +
+ {/* Dot indicators */} +
+ {agents.map((_, index) => ( +
- {/* Dot indicators */} -
- {agents.map((_, index) => ( -
-
+ )}
diff --git a/src/components/home/TopProfessionals.tsx b/src/components/home/TopProfessionals.tsx index 73b80a8..1233b8a 100644 --- a/src/components/home/TopProfessionals.tsx +++ b/src/components/home/TopProfessionals.tsx @@ -3,7 +3,6 @@ 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'; @@ -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 async function resolveAvatarUrl(avatar: string | null): Promise { if (!avatar) return '/assets/icons/user-placeholder-icon.svg'; @@ -271,76 +196,47 @@ export function TopProfessionals({ content }: { content?: TopProfessionalsConten const [loading, setLoading] = useState(true); useEffect(() => { - const fetchProfessionals = async () => { + const loadProfessionals = 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 => { + // Map CMS data to display format, resolving avatar URLs + const mapCmsEntries = async ( + entries: TopProfessionalsContent['agents'], + ): 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.` - : ''; - + entries.map(async (entry, index) => { + const imageUrl = await resolveAvatarUrl(entry.imageUrl || null); return { - id: profile.id, - name: `${profile.firstName} ${profile.lastName}`.trim(), - subtitle: profile.agentType ? `(${profile.agentType.name})` : '', - location, - experience: experienceYears, - expertise, + id: `cms-${index}`, + name: entry.name, + subtitle: entry.subtitle || '', + location: entry.location || '', + experience: entry.experience || '', + expertise: entry.expertise || [], imageUrl, - isVerified: profile.isVerified, - rating: profile.rating, - slug: profile.slug, + isVerified: false, + rating: null, + slug: '', }; - }) + }), ); }; const [mappedAgents, mappedLenders] = await Promise.all([ - mapProfiles(agentsResponse.data), - mapProfiles(lendersResponse.data), + mapCmsEntries(data.agents || []), + mapCmsEntries(data.lenders || []), ]); setAgents(mappedAgents); setLenders(mappedLenders); } catch (err) { - console.error('Failed to fetch top professionals:', err); + console.error('Failed to load top professionals:', err); } finally { setLoading(false); } }; - fetchProfessionals(); - }, []); + loadProfessionals(); + }, [data.agents, data.lenders]); const displayData = activeTab === 'agents' ? agents : lenders; diff --git a/src/services/agents.service.ts b/src/services/agents.service.ts index e43ad4a..4b094da 100644 --- a/src/services/agents.service.ts +++ b/src/services/agents.service.ts @@ -191,6 +191,11 @@ class AgentsService { return response.data.data; } + async getFeaturedAgents(limit = 3): Promise { + const response = await api.get>(`/agents/featured?limit=${limit}`); + return response.data.data; + } + async searchAgents(params: SearchAgentsParams = {}): Promise { const queryParams = new URLSearchParams();