'use client'; import { useState, useEffect, useCallback, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import Image from 'next/image'; import Link from 'next/link'; import { FilterModal, FilterState, FilterField } from '@/components/profiles/FilterModal'; import { agentsService, AgentType, PublicAgentProfile, SearchAgentsParams } from '@/services/agents.service'; import { profileSectionsService, FilterableField } from '@/services/profile-sections.service'; import { uploadService } from '@/services/upload.service'; interface FilterSectionProps { title: string; options: { label: string; value: string }[]; selectedOptions: string[]; onToggle: (optionValue: string) => void; showMore?: boolean; } function FilterSection({ title, options, selectedOptions, onToggle, showMore }: FilterSectionProps) { const [expanded, setExpanded] = useState(false); const displayOptions = expanded ? options : options.slice(0, 6); return (

{title}

{displayOptions.map((option) => ( ))}
{showMore && options.length > 6 && ( )}
); } interface ProfileCardProps { profile: PublicAgentProfile; resolvedAvatarUrl?: string | null; } // Profile image with shimmer loading function ProfileImage({ src, alt, className }: { src: string | null; alt: string; className?: string }) { const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>(src ? 'loading' : 'error'); useEffect(() => { setStatus(src ? 'loading' : 'error'); }, [src]); return (
{status === 'loading' && (
)} {src && status !== 'error' ? ( /* eslint-disable-next-line @next/next/no-img-element */ {alt} setStatus('loaded')} onError={() => setStatus('error')} /> ) : status === 'error' ? (
{alt}
) : null}
); } function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) { const [showFullBio, setShowFullBio] = useState(false); const [showAllTags, setShowAllTags] = useState(false); const getProfileImageUrl = (): string | null => { if (resolvedAvatarUrl) return resolvedAvatarUrl; if (profile.avatar?.startsWith('/')) return profile.avatar; return null; }; // Format member since date const formatMemberSince = (dateString?: string) => { if (!dateString) return 'Member'; try { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); } catch { return 'Member'; } }; // Get location string - check dynamic field values first, then fallback to legacy columns const getLocation = () => { let cityValue: string | null = null; let stateValue: string | null = null; // Helper to format snake_case to Title Case const formatValue = (v: string) => v.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' '); // First, check fieldValues for state and city (dynamic profile fields) 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'); if (stateField && stateField.jsonValue) { const value = stateField.jsonValue; if (Array.isArray(value) && value.length > 0) { // For multiselect, join all selected values stateValue = value.map(v => formatValue(String(v))).join(', '); } else if (typeof value === 'string' && value.trim()) { stateValue = formatValue(value); } } if (cityField && cityField.jsonValue) { const value = cityField.jsonValue; if (Array.isArray(value) && value.length > 0) { // For multiselect, join all selected values cityValue = value.map(v => formatValue(String(v))).join(', '); } else if (typeof value === 'string' && value.trim()) { cityValue = formatValue(value); } } } // Fallback to legacy profile columns if dynamic fields not found if (!cityValue && profile.city) { cityValue = profile.city; } if (!stateValue && profile.state) { stateValue = profile.state; } // Build location string if (cityValue && stateValue) { return `${cityValue}, ${stateValue}`; } if (cityValue && profile.country) { return `${cityValue}, ${profile.country}`; } if (stateValue && profile.country) { return `${stateValue}, ${profile.country}`; } if (cityValue) { return cityValue; } if (stateValue) { return stateValue; } if (profile.country) { return profile.country; } if (profile.serviceAreas && profile.serviceAreas.length > 0) { return profile.serviceAreas[0]; } return null; }; // Extract expertise tags from fieldValues - only include expertise-related fields const getExpertiseTags = (): string[] => { const tags: string[] = []; // Only include expertise-related field slugs (matching profileDataMapper.ts) const expertiseFieldSlugs = [ 'about_me_expertise', 'expertise_areas', 'specializations', 'areas_of_expertise', 'expertise', ]; // Add from fieldValues - only expertise-related fields if (profile.fieldValues && profile.fieldValues.length > 0) { profile.fieldValues.forEach((fv) => { // Only include expertise-related fields if (!expertiseFieldSlugs.includes(fv.field.slug)) { return; } const value = fv.jsonValue; if (Array.isArray(value)) { // For multi-select fields, add all selected values value.forEach((v) => { if (typeof v === 'string' && v.trim()) { // Convert snake_case to Title Case for display 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); } } }); } // Add from specializations array (legacy field) if (profile.specializations && profile.specializations.length > 0) { profile.specializations.forEach((spec) => { if (!tags.includes(spec)) { tags.push(spec); } }); } // Add from languages array if (profile.languages && profile.languages.length > 0) { profile.languages.forEach((lang) => { if (!tags.includes(lang)) { tags.push(lang); } }); } return tags; }; // Get description from fieldValues (description field) or fallback to profile.bio const getDescription = (): string | null => { if (profile.fieldValues && profile.fieldValues.length > 0) { const descField = profile.fieldValues.find(fv => fv.field.slug === 'description'); if (descField && descField.textValue) { return descField.textValue; } } return profile.bio; }; const location = getLocation(); const expertiseTags = getExpertiseTags(); const description = getDescription(); const truncatedDescription = description && description.length > 200 ? description.slice(0, 200) + '...' : description || ''; return (
{/* Profile Image & View Profile Button */}
{/* Profile Info */}
{/* Header Row - Name, Verified, Match Badge */}
{/* Name and Verified Badge */}

{profile.firstName}{' '} {profile.lastName}

{profile.isVerified && ( <> Verified (Verified Expert) )}
{/* Agent Type */}

{profile.agentType?.name || 'Real Estate Professional'}

{/* Match Badge */}
95% Match
{/* Location & Member Since */}
{location && (
Location {location}
)}
Member Since Member Since {formatMemberSince(profile.createdAt)}
{/* Description */} {description && (

{showFullBio ? description : truncatedDescription} {description.length > 200 && ( <> {' '} )}

)} {/* Expertise Tags */} {expertiseTags.length > 0 && (

Expertise:

{(showAllTags ? expertiseTags : expertiseTags.slice(0, 8)).map((tag, index) => ( {tag} ))} {expertiseTags.length > 8 && ( )}
)}
); } function ProfilesPageContent() { const searchParams = useSearchParams(); // Get URL params directly for use in effects const typeFromUrl = searchParams.get('type'); const nameFromUrl = searchParams.get('name') || ''; const locationFromUrl = searchParams.get('location') || ''; // State for API data const [agents, setAgents] = useState([]); const [avatarUrls, setAvatarUrls] = useState>({}); const [agentTypes, setAgentTypes] = useState([]); const [filterFields, setFilterFields] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [totalPages, setTotalPages] = useState(1); const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); // State for search and filters - initialize from URL params const [searchQuery, setSearchQuery] = useState(nameFromUrl); const [activeTypeId, setActiveTypeId] = useState(typeFromUrl); const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents'); const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [filters, setFilters] = useState({}); // Sync state when URL params change (e.g., browser back/forward, client-side navigation) useEffect(() => { setActiveTypeId(typeFromUrl); setSearchQuery(nameFromUrl); }, [typeFromUrl, nameFromUrl]); // Fetch agent types and filter fields on mount useEffect(() => { const fetchInitialData = async () => { try { const [types, fields] = await Promise.all([ agentsService.getAgentTypes(), profileSectionsService.getFilterableFields(), ]); setAgentTypes(types); // Convert FilterableField to FilterField format const filterFieldsData: FilterField[] = fields.map((f: FilterableField) => ({ slug: f.slug, name: f.name, fieldType: f.fieldType, options: f.options, })); setFilterFields(filterFieldsData); } catch (error) { console.error('Failed to fetch initial data:', error); } }; fetchInitialData(); }, []); // Fetch agents based on filters const fetchAgents = useCallback(async () => { try { setLoading(true); setError(null); const params: SearchAgentsParams = { page: currentPage, limit: 10, sortBy: 'createdAt', sortOrder: 'desc', }; // Add search query if (searchQuery) { params.search = searchQuery; } // Add agent type filter if (activeTypeId) { params.agentTypeId = activeTypeId; } // Add location search (searches city, state, country, and dynamic field values) if (locationFromUrl) { params.location = locationFromUrl; } // Add dynamic profile field filters const activeFilters = Object.entries(filters).reduce((acc, [key, values]) => { if (values && values.length > 0) { acc[key] = values; } return acc; }, {} as Record); if (Object.keys(activeFilters).length > 0) { params.filters = activeFilters; } const response = await agentsService.searchAgents(params); // Resolve all avatar URLs via presigned-download-url API const urlMap: Record = {}; const avatarPromises = response.data.map(async (agent) => { if (agent.avatar && !agent.avatar.startsWith('http') && !agent.avatar.startsWith('/')) { try { const presignedUrl = await uploadService.getPresignedDownloadUrl(agent.avatar); urlMap[agent.id] = presignedUrl; } catch { // Skip failed URLs, card will show placeholder } } else if (agent.avatar) { urlMap[agent.id] = agent.avatar; } }); await Promise.all(avatarPromises); setAvatarUrls(urlMap); setAgents(response.data); setTotalPages(response.totalPages); setTotalResults(response.total); } catch (err) { console.error('Failed to fetch agents:', err); setError('Failed to load agents. Please try again.'); } finally { setLoading(false); } }, [currentPage, searchQuery, activeTypeId, locationFromUrl, filters]); // Fetch agents when filters change useEffect(() => { fetchAgents(); }, [fetchAgents]); const toggleFilter = (fieldSlug: string, optionValue: string) => { setFilters(prev => { const currentValues = prev[fieldSlug] || []; const newValues = currentValues.includes(optionValue) ? currentValues.filter(v => v !== optionValue) : [...currentValues, optionValue]; return { ...prev, [fieldSlug]: newValues }; }); }; const clearAllFilters = () => { setFilters({}); }; const applyFilters = () => { setIsFilterModalOpen(false); setCurrentPage(1); // Reset to first page when applying filters // Filters are applied via the fetchAgents effect }; const handleSearch = () => { setCurrentPage(1); fetchAgents(); }; const handleTypeChange = (typeId: string | null) => { setActiveTypeId(typeId); setCurrentPage(1); // Sync listing type sidebar with agent type tab selection if (typeId) { const matchedType = agentTypes.find(t => t.id === typeId); if (matchedType?.name === 'Lender') { setListingType('lenders'); } else { setListingType('agents'); } } }; const handleListingTypeChange = (type: 'agents' | 'lenders') => { setListingType(type); // Map listing type to the corresponding agent type and filter const typeName = type === 'agents' ? 'Professional' : 'Lender'; const matchedType = agentTypes.find(t => t.name === typeName); setActiveTypeId(matchedType?.id || null); setCurrentPage(1); }; // Get filter field by slug for sidebar const getFilterFieldBySlug = (slug: string): FilterField | undefined => { return filterFields.find(f => f.slug === slug); }; // Define which fields to show in sidebar (main filter categories) const sidebarFilterSlugs = ['client_specialization', 'loan_type', 'property_type', 'price_point']; return (
{/* Left Sidebar - hidden on mobile */}
{/* Listing Type */}

Listing Type

{/* Filters */}

Filters

{/* Dynamic Filter Sections */} {sidebarFilterSlugs.map(slug => { const field = getFilterFieldBySlug(slug); if (!field) return null; return ( toggleFilter(field.slug, optionValue)} showMore={field.options.length > 6} /> ); })} {filterFields.length === 0 && (

Loading filters...

)}
{/* Main Content */}
{/* Mobile Filter Button - visible only on mobile */}

Agent Profiles

{/* Search Bar */}
setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} className="w-full h-[44px] px-4 pr-12 rounded-[10px] border border-[#00293d]/20 bg-white font-serif text-[14px] text-[#00293d] placeholder:text-[#00293d]/50 focus:outline-none focus:border-[#e58625]" />
{/* Agent Type Tabs */}
{agentTypes.map((type) => ( ))}
{/* Loading State */} {loading && (

Loading agents...

)} {/* Error State */} {error && !loading && (

{error}

)} {/* Empty State */} {!loading && !error && agents.length === 0 && (

No agents found

Try adjusting your search criteria or filters

)} {/* Profile Cards */} {!loading && !error && agents.length > 0 && (
{agents.map((profile) => ( ))}
)} {/* Pagination */} {!loading && !error && totalPages > 1 && (
Page {currentPage} of {totalPages}
)}
{/* Filter Modal */} setIsFilterModalOpen(false)} filters={filters} filterFields={filterFields} onToggleFilter={toggleFilter} onClearAll={clearAllFilters} onApply={applyFilters} />
); } export default function ProfilesPage() { return (

Loading...

}> ); }