'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';
import { MobileBackButton } from '@/components/layout/MobileBackButton';
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 */
![]()
{ if (el?.complete) setStatus(el.naturalWidth > 0 ? 'loaded' : 'error'); }}
src={src}
alt={alt}
className={`w-full h-full object-cover object-top transition-opacity duration-300 ${status === 'loaded' ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setStatus('loaded')}
onError={() => setStatus('error')}
/>
) : status === 'error' ? (
) : 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 (state first, then city)
if (cityValue && stateValue) {
return `${stateValue}, ${cityValue}`;
}
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 maxDescLength = 150;
const truncatedDescription = description && description.length > maxDescLength
? description.slice(0, maxDescLength) + '...'
: description || '';
const INITIAL_TAG_COUNT = 5;
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 Expert)
>
)}
{/* Agent Type */}
{profile.agentType?.name || 'Real Estate Professional'}
{/* Match Badge */}
{profile.matchPercentage && (
= 80 ? 'bg-[#638559]' : profile.matchPercentage >= 65 ? 'bg-[#7d917d]' : 'bg-[#00293d]/60'
}`}>
{profile.matchPercentage}% Match
)}
{/* Location & Member Since */}
{location && (
{location}
)}
Member Since {formatMemberSince(profile.createdAt)}
{/* Description */}
{description && (
{showFullBio ? description : truncatedDescription}
{description.length > maxDescLength && (
<>
{' '}
>
)}
)}
{/* Expertise Tags */}
{expertiseTags.length > 0 && (
Expertise:
{(showAllTags ? expertiseTags : expertiseTags.slice(0, INITIAL_TAG_COUNT)).map((tag, index) => (
{tag}
))}
{expertiseTags.length > INITIAL_TAG_COUNT && (
)}
)}
);
}
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') || '';
const filtersFromUrl = searchParams.get('filters') || '';
const listingFromUrl = searchParams.get('listing') as 'agents' | 'lenders' | null;
// 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'>(listingFromUrl || 'agents');
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
const [filters, setFilters] = useState(() => {
if (filtersFromUrl) {
try {
return JSON.parse(filtersFromUrl) as FilterState;
} catch {
return {};
}
}
return {};
});
// Sync state when URL params change (e.g., browser back/forward, client-side navigation)
useEffect(() => {
setActiveTypeId(typeFromUrl);
setSearchQuery(nameFromUrl);
if (filtersFromUrl) {
try {
setFilters(JSON.parse(filtersFromUrl) as FilterState);
} catch {
// ignore invalid JSON
}
}
}, [typeFromUrl, nameFromUrl, filtersFromUrl]);
// 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);
// If listing param is set, auto-select the matching agent type
if (listingFromUrl && !typeFromUrl) {
const typeName = listingFromUrl === 'lenders' ? 'Lender' : 'Professional';
const matchedType = types.find((t: AgentType) => t.name === typeName);
if (matchedType) {
setActiveTypeId(matchedType.id);
}
}
// 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 && (
)}
{/* Main Content */}
{/* Mobile Filter Button - visible only on mobile */}
Agent Profiles
{/* Search Bar */}
{/* Agent Type Tabs */}
{agentTypes.map((type) => (
))}
{/* Loading State */}
{loading && (
)}
{/* Error State */}
{error && !loading && (
)}
{/* 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 (
}>
);
}