2026-01-20 11:51:10 +05:30
|
|
|
'use client';
|
|
|
|
|
|
2026-01-25 00:21:17 +05:30
|
|
|
import { useState, useEffect, useCallback, Suspense } from 'react';
|
2026-01-20 11:51:10 +05:30
|
|
|
import { useSearchParams } from 'next/navigation';
|
|
|
|
|
import Image from 'next/image';
|
|
|
|
|
import Link from 'next/link';
|
2026-02-01 15:04:51 +05:30
|
|
|
import { FilterModal, FilterState, FilterField } from '@/components/profiles/FilterModal';
|
2026-01-25 00:21:17 +05:30
|
|
|
import { agentsService, AgentType, PublicAgentProfile, SearchAgentsParams } from '@/services/agents.service';
|
2026-02-01 15:04:51 +05:30
|
|
|
import { profileSectionsService, FilterableField } from '@/services/profile-sections.service';
|
2026-01-25 00:21:17 +05:30
|
|
|
import { uploadService } from '@/services/upload.service';
|
2026-01-20 11:51:10 +05:30
|
|
|
|
|
|
|
|
interface FilterSectionProps {
|
|
|
|
|
title: string;
|
2026-02-01 15:04:51 +05:30
|
|
|
options: { label: string; value: string }[];
|
2026-01-20 11:51:10 +05:30
|
|
|
selectedOptions: string[];
|
2026-02-01 15:04:51 +05:30
|
|
|
onToggle: (optionValue: string) => void;
|
2026-01-20 11:51:10 +05:30
|
|
|
showMore?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function FilterSection({ title, options, selectedOptions, onToggle, showMore }: FilterSectionProps) {
|
|
|
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
|
const displayOptions = expanded ? options : options.slice(0, 6);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="pb-4 mb-4">
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
<h3 className="font-fractul font-semibold text-[14px] text-[#00293d]">{title}</h3>
|
|
|
|
|
<button className="text-[#00293d]">
|
|
|
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
|
|
|
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{displayOptions.map((option) => (
|
2026-02-01 15:04:51 +05:30
|
|
|
<label key={option.value} className="flex items-center gap-2 cursor-pointer">
|
2026-01-20 11:51:10 +05:30
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
2026-02-01 15:04:51 +05:30
|
|
|
checked={selectedOptions.includes(option.value)}
|
|
|
|
|
onChange={() => onToggle(option.value)}
|
2026-01-20 11:51:10 +05:30
|
|
|
className="w-4 h-4 rounded border-[#00293d]/30 text-[#e58625] focus:ring-[#e58625]"
|
|
|
|
|
/>
|
2026-02-01 15:04:51 +05:30
|
|
|
<span className="font-serif text-[13px] text-[#00293d]">{option.label}</span>
|
2026-01-20 11:51:10 +05:30
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{showMore && options.length > 6 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
|
|
|
|
className="mt-2 font-serif text-[13px] text-[#e58625] hover:underline"
|
|
|
|
|
>
|
|
|
|
|
{expanded ? 'Show Less' : 'Show More'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ProfileCardProps {
|
2026-01-25 00:21:17 +05:30
|
|
|
profile: PublicAgentProfile;
|
2026-01-20 11:51:10 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProfileCard({ profile }: ProfileCardProps) {
|
|
|
|
|
const [showFullBio, setShowFullBio] = useState(false);
|
2026-01-25 00:21:17 +05:30
|
|
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
|
|
|
|
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
|
|
|
|
|
|
|
|
|
|
const truncatedBio = profile.bio && profile.bio.length > 150
|
|
|
|
|
? profile.bio.slice(0, 150) + '...'
|
|
|
|
|
: profile.bio || '';
|
|
|
|
|
|
|
|
|
|
// Fetch presigned URL for avatar if it's an S3 key
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchAvatarUrl = async () => {
|
|
|
|
|
if (profile.avatar && !profile.avatar.startsWith('http') && !profile.avatar.startsWith('/')) {
|
|
|
|
|
try {
|
|
|
|
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
|
|
|
|
setAvatarUrl(presignedUrl);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to get avatar URL:', error);
|
|
|
|
|
}
|
|
|
|
|
} else if (profile.avatar) {
|
|
|
|
|
setAvatarUrl(profile.avatar);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
fetchAvatarUrl();
|
|
|
|
|
}, [profile.avatar]);
|
|
|
|
|
|
|
|
|
|
const getProfileImageUrl = () => {
|
|
|
|
|
if (avatarUrl) return avatarUrl;
|
|
|
|
|
if (profile.avatar?.startsWith('/')) return profile.avatar;
|
|
|
|
|
return defaultImage;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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';
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-20 11:51:10 +05:30
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-white rounded-[15px] p-4 flex gap-4 shadow-[0px_4px_20px_rgba(0,0,0,0.08)]">
|
|
|
|
|
{/* Profile Image */}
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
<div className="relative w-[120px] h-[140px] rounded-[10px] overflow-hidden">
|
2026-01-25 00:21:17 +05:30
|
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
|
|
|
<img
|
|
|
|
|
src={getProfileImageUrl()}
|
|
|
|
|
alt={`${profile.firstName} ${profile.lastName}`}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
onError={(e) => {
|
|
|
|
|
const target = e.target as HTMLImageElement;
|
|
|
|
|
target.src = defaultImage;
|
|
|
|
|
}}
|
2026-01-20 11:51:10 +05:30
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Link href={`/user/profile/${profile.id}`}>
|
|
|
|
|
<button className="w-full mt-3 px-6 py-2.5 bg-[#e58625] hover:bg-[#d47720] text-[#00293d] font-fractul font-bold text-[14px] rounded-[15px] transition-colors">
|
|
|
|
|
View Profile
|
|
|
|
|
</button>
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Profile Info */}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-start justify-between mb-1">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
2026-01-25 00:21:17 +05:30
|
|
|
<h3 className="font-fractul font-bold text-[16px] text-[#00293d]">
|
|
|
|
|
{profile.firstName} {profile.lastName}
|
|
|
|
|
</h3>
|
|
|
|
|
{profile.isVerified && (
|
2026-01-20 11:51:10 +05:30
|
|
|
<span className="flex items-center gap-1 text-[#e58625] font-serif text-[12px]">
|
|
|
|
|
<Image
|
|
|
|
|
src="/assets/icons/verified-badge-blue.svg"
|
|
|
|
|
alt="Verified"
|
|
|
|
|
width={14}
|
|
|
|
|
height={14}
|
|
|
|
|
/>
|
|
|
|
|
(Verified Expert)
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-01-25 00:21:17 +05:30
|
|
|
<p className="font-serif text-[13px] text-[#00293d] mt-0.5">
|
|
|
|
|
{profile.agentType?.name || 'Real Estate Professional'}
|
|
|
|
|
</p>
|
2026-01-20 11:51:10 +05:30
|
|
|
</div>
|
2026-01-25 00:21:17 +05:30
|
|
|
{profile.rating && (
|
|
|
|
|
<div className="bg-[#e58625] text-white px-3 py-1 rounded-full font-fractul font-bold text-[12px]">
|
|
|
|
|
{profile.rating.toFixed(1)} Rating
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-20 11:51:10 +05:30
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Location & Member Since */}
|
|
|
|
|
<div className="flex items-center gap-4 mt-2">
|
2026-01-25 00:21:17 +05:30
|
|
|
{profile.serviceAreas && profile.serviceAreas.length > 0 && (
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<Image
|
|
|
|
|
src="/assets/icons/location-pin-orange.svg"
|
|
|
|
|
alt="Location"
|
|
|
|
|
width={12}
|
|
|
|
|
height={15}
|
|
|
|
|
/>
|
|
|
|
|
<span className="font-serif text-[12px] text-[#00293d]">{profile.serviceAreas[0]}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-20 11:51:10 +05:30
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<Image
|
|
|
|
|
src="/assets/icons/calendar-icon.svg"
|
|
|
|
|
alt="Member Since"
|
|
|
|
|
width={14}
|
|
|
|
|
height={14}
|
|
|
|
|
/>
|
2026-01-25 00:21:17 +05:30
|
|
|
<span className="font-serif text-[12px] text-[#00293d]">
|
|
|
|
|
Member Since {formatMemberSince(profile.createdAt)}
|
|
|
|
|
</span>
|
2026-01-20 11:51:10 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Bio */}
|
2026-01-25 00:21:17 +05:30
|
|
|
{profile.bio && (
|
|
|
|
|
<p className="font-serif text-[13px] text-[#00293d] mt-3 leading-relaxed">
|
|
|
|
|
{showFullBio ? profile.bio : truncatedBio}
|
|
|
|
|
{profile.bio.length > 150 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowFullBio(!showFullBio)}
|
|
|
|
|
className="text-[#e58625] ml-1 hover:underline font-medium"
|
|
|
|
|
>
|
|
|
|
|
{showFullBio ? 'Show Less' : 'Show More.'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2026-01-20 11:51:10 +05:30
|
|
|
|
2026-01-25 00:21:17 +05:30
|
|
|
{/* Specializations */}
|
|
|
|
|
{profile.specializations && profile.specializations.length > 0 && (
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
<p className="font-fractul font-semibold text-[12px] text-[#00293d] mb-2">Expertise:</p>
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{profile.specializations.map((tag, index) => (
|
|
|
|
|
<span
|
|
|
|
|
key={index}
|
|
|
|
|
className="border border-[#00293d]/30 rounded-full px-3 py-1 font-serif text-[11px] text-[#00293d]"
|
|
|
|
|
>
|
|
|
|
|
{tag}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Languages */}
|
|
|
|
|
{profile.languages && profile.languages.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
|
|
|
{profile.languages.map((lang, index) => (
|
2026-01-20 11:51:10 +05:30
|
|
|
<span
|
|
|
|
|
key={index}
|
|
|
|
|
className="border border-[#00293d]/30 rounded-full px-3 py-1 font-serif text-[11px] text-[#00293d]"
|
|
|
|
|
>
|
2026-01-25 00:21:17 +05:30
|
|
|
{lang}
|
2026-01-20 11:51:10 +05:30
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-01-25 00:21:17 +05:30
|
|
|
)}
|
2026-01-20 11:51:10 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 00:21:17 +05:30
|
|
|
function ProfilesPageContent() {
|
2026-01-20 11:51:10 +05:30
|
|
|
const searchParams = useSearchParams();
|
2026-01-25 00:21:17 +05:30
|
|
|
|
|
|
|
|
// State for API data
|
|
|
|
|
const [agents, setAgents] = useState<PublicAgentProfile[]>([]);
|
|
|
|
|
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
2026-02-01 15:04:51 +05:30
|
|
|
const [filterFields, setFilterFields] = useState<FilterField[]>([]);
|
2026-01-25 00:21:17 +05:30
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
const [totalResults, setTotalResults] = useState(0);
|
|
|
|
|
|
2026-02-01 15:04:51 +05:30
|
|
|
// State for search and filters - initialize from URL params
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState(() => searchParams.get('name') || '');
|
|
|
|
|
const [activeTypeId, setActiveTypeId] = useState<string | null>(() => searchParams.get('type'));
|
|
|
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
2026-01-20 11:51:10 +05:30
|
|
|
const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents');
|
2026-01-20 11:59:57 +05:30
|
|
|
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
2026-02-01 15:04:51 +05:30
|
|
|
const [filters, setFilters] = useState<FilterState>({});
|
|
|
|
|
|
|
|
|
|
// Mark as initialized after first render and sync with URL param changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setIsInitialized(true);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Sync state when URL params change (e.g., browser back/forward)
|
2026-01-25 00:21:17 +05:30
|
|
|
useEffect(() => {
|
2026-02-01 15:04:51 +05:30
|
|
|
if (isInitialized) {
|
|
|
|
|
const type = searchParams.get('type');
|
|
|
|
|
const name = searchParams.get('name');
|
2026-01-25 00:21:17 +05:30
|
|
|
|
2026-02-01 15:04:51 +05:30
|
|
|
setActiveTypeId(type);
|
|
|
|
|
setSearchQuery(name || '');
|
|
|
|
|
}
|
|
|
|
|
}, [searchParams, isInitialized]);
|
2026-01-25 00:21:17 +05:30
|
|
|
|
2026-02-01 15:04:51 +05:30
|
|
|
// Fetch agent types and filter fields on mount
|
2026-01-25 00:21:17 +05:30
|
|
|
useEffect(() => {
|
2026-02-01 15:04:51 +05:30
|
|
|
const fetchInitialData = async () => {
|
2026-01-25 00:21:17 +05:30
|
|
|
try {
|
2026-02-01 15:04:51 +05:30
|
|
|
const [types, fields] = await Promise.all([
|
|
|
|
|
agentsService.getAgentTypes(),
|
|
|
|
|
profileSectionsService.getFilterableFields(),
|
|
|
|
|
]);
|
2026-01-25 00:21:17 +05:30
|
|
|
setAgentTypes(types);
|
2026-02-01 15:04:51 +05:30
|
|
|
|
|
|
|
|
// 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);
|
2026-01-25 00:21:17 +05:30
|
|
|
} catch (error) {
|
2026-02-01 15:04:51 +05:30
|
|
|
console.error('Failed to fetch initial data:', error);
|
2026-01-25 00:21:17 +05:30
|
|
|
}
|
|
|
|
|
};
|
2026-02-01 15:04:51 +05:30
|
|
|
fetchInitialData();
|
2026-01-25 00:21:17 +05:30
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 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 from URL params
|
|
|
|
|
const location = searchParams.get('location');
|
|
|
|
|
if (location) {
|
|
|
|
|
// Try to set as city first
|
|
|
|
|
params.city = location;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await agentsService.searchAgents(params);
|
|
|
|
|
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, searchParams]);
|
|
|
|
|
|
2026-02-01 15:04:51 +05:30
|
|
|
// Fetch agents when filters change (only after initialization)
|
2026-01-25 00:21:17 +05:30
|
|
|
useEffect(() => {
|
2026-02-01 15:04:51 +05:30
|
|
|
if (isInitialized) {
|
|
|
|
|
fetchAgents();
|
|
|
|
|
}
|
|
|
|
|
}, [fetchAgents, isInitialized]);
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
});
|
2026-01-20 11:51:10 +05:30
|
|
|
};
|
|
|
|
|
|
2026-01-20 11:59:57 +05:30
|
|
|
const clearAllFilters = () => {
|
2026-02-01 15:04:51 +05:30
|
|
|
setFilters({});
|
2026-01-20 11:59:57 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const applyFilters = () => {
|
|
|
|
|
setIsFilterModalOpen(false);
|
2026-01-25 00:21:17 +05:30
|
|
|
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);
|
2026-01-20 11:59:57 +05:30
|
|
|
};
|
|
|
|
|
|
2026-02-01 15:04:51 +05:30
|
|
|
// 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'];
|
|
|
|
|
|
2026-01-20 11:51:10 +05:30
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-white">
|
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
|
|
|
<div className="flex gap-6">
|
|
|
|
|
{/* Left Sidebar */}
|
|
|
|
|
<div className="w-[220px] flex-shrink-0">
|
|
|
|
|
{/* Listing Type */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h2 className="font-fractul font-bold text-[14px] text-[#00293d] mb-3">Listing Type</h2>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setListingType('agents')}
|
|
|
|
|
className={`block font-serif text-[14px] ${
|
|
|
|
|
listingType === 'agents' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
Agents
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setListingType('lenders')}
|
|
|
|
|
className={`block font-serif text-[14px] ${
|
|
|
|
|
listingType === 'lenders' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
Lenders
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Filters */}
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
|
|
<h2 className="font-fractul font-bold text-[14px] text-[#00293d]">Filters</h2>
|
2026-01-20 11:59:57 +05:30
|
|
|
<button
|
|
|
|
|
onClick={() => setIsFilterModalOpen(true)}
|
|
|
|
|
className="text-[#00293d] hover:opacity-70"
|
|
|
|
|
>
|
2026-01-20 11:51:10 +05:30
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
|
|
|
<path d="M3 6H21M7 12H17M11 18H13" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-01 15:04:51 +05:30
|
|
|
{/* Dynamic Filter Sections */}
|
|
|
|
|
{sidebarFilterSlugs.map(slug => {
|
|
|
|
|
const field = getFilterFieldBySlug(slug);
|
|
|
|
|
if (!field) return null;
|
|
|
|
|
return (
|
|
|
|
|
<FilterSection
|
|
|
|
|
key={field.slug}
|
|
|
|
|
title={field.name}
|
|
|
|
|
options={field.options}
|
|
|
|
|
selectedOptions={filters[field.slug] || []}
|
|
|
|
|
onToggle={(optionValue) => toggleFilter(field.slug, optionValue)}
|
|
|
|
|
showMore={field.options.length > 6}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-01-20 11:51:10 +05:30
|
|
|
|
2026-02-01 15:04:51 +05:30
|
|
|
{filterFields.length === 0 && (
|
|
|
|
|
<div className="py-4">
|
|
|
|
|
<p className="font-serif text-[13px] text-[#00293d]/50">Loading filters...</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-20 11:51:10 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Main Content */}
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
{/* Search Bar */}
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Search Agents"
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
2026-01-25 00:21:17 +05:30
|
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
2026-01-20 11:51:10 +05:30
|
|
|
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]"
|
|
|
|
|
/>
|
2026-01-25 00:21:17 +05:30
|
|
|
<button
|
|
|
|
|
onClick={handleSearch}
|
|
|
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 w-[36px] h-[36px] bg-[#e58625] rounded-[8px] flex items-center justify-center"
|
|
|
|
|
>
|
2026-01-20 11:51:10 +05:30
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
|
|
|
<path d="M21 21L16.65 16.65M19 11C19 15.4183 15.4183 19 11 19C6.58172 19 3 15.4183 3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-25 00:21:17 +05:30
|
|
|
{/* Agent Type Tabs */}
|
2026-01-20 11:51:10 +05:30
|
|
|
<div className="flex flex-wrap gap-2 mb-6">
|
2026-01-25 00:21:17 +05:30
|
|
|
<button
|
|
|
|
|
onClick={() => handleTypeChange(null)}
|
|
|
|
|
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${
|
|
|
|
|
activeTypeId === null
|
|
|
|
|
? 'bg-[#00293d] text-white border-[#00293d]'
|
|
|
|
|
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
All Types
|
|
|
|
|
</button>
|
|
|
|
|
{agentTypes.map((type) => (
|
2026-01-20 11:51:10 +05:30
|
|
|
<button
|
2026-01-25 00:21:17 +05:30
|
|
|
key={type.id}
|
|
|
|
|
onClick={() => handleTypeChange(type.id)}
|
2026-01-20 11:51:10 +05:30
|
|
|
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${
|
2026-01-25 00:21:17 +05:30
|
|
|
activeTypeId === type.id
|
2026-01-20 11:51:10 +05:30
|
|
|
? 'bg-[#00293d] text-white border-[#00293d]'
|
|
|
|
|
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2026-01-25 00:21:17 +05:30
|
|
|
{type.name}
|
2026-01-20 11:51:10 +05:30
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-25 00:21:17 +05:30
|
|
|
{/* Results Count */}
|
|
|
|
|
{!loading && (
|
|
|
|
|
<p className="font-serif text-[13px] text-[#00293d]/70 mb-4">
|
|
|
|
|
{totalResults} {totalResults === 1 ? 'agent' : 'agents'} found
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Loading State */}
|
|
|
|
|
{loading && (
|
|
|
|
|
<div className="flex items-center justify-center py-12">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#E58625]"></div>
|
|
|
|
|
<p className="text-[14px] font-serif text-[#00293D]/70">Loading agents...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Error State */}
|
|
|
|
|
{error && !loading && (
|
|
|
|
|
<div className="bg-red-50 border border-red-200 rounded-[15px] p-6 text-center">
|
|
|
|
|
<p className="text-red-600 font-serif text-[14px]">{error}</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={fetchAgents}
|
|
|
|
|
className="mt-4 px-6 py-2 bg-[#E58625] rounded-full text-[14px] font-semibold font-serif text-white hover:bg-[#E58625]/90 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Try Again
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Empty State */}
|
|
|
|
|
{!loading && !error && agents.length === 0 && (
|
|
|
|
|
<div className="bg-gray-50 rounded-[15px] p-12 text-center">
|
|
|
|
|
<div className="w-16 h-16 bg-[#E58625]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<svg className="w-8 h-8 text-[#E58625]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 className="font-fractul font-bold text-[18px] text-[#00293d] mb-2">No agents found</h3>
|
|
|
|
|
<p className="font-serif text-[14px] text-[#00293d]/70">
|
|
|
|
|
Try adjusting your search criteria or filters
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-20 11:51:10 +05:30
|
|
|
{/* Profile Cards */}
|
2026-01-25 00:21:17 +05:30
|
|
|
{!loading && !error && agents.length > 0 && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{agents.map((profile) => (
|
|
|
|
|
<ProfileCard key={profile.id} profile={profile} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Pagination */}
|
|
|
|
|
{!loading && !error && totalPages > 1 && (
|
|
|
|
|
<div className="flex justify-center gap-2 mt-8">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
className="px-4 py-2 rounded-[10px] border border-[#00293d]/20 font-serif text-[13px] text-[#00293d] disabled:opacity-50 disabled:cursor-not-allowed hover:border-[#00293d]/40 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Previous
|
|
|
|
|
</button>
|
|
|
|
|
<span className="px-4 py-2 font-serif text-[13px] text-[#00293d]">
|
|
|
|
|
Page {currentPage} of {totalPages}
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
|
|
|
|
disabled={currentPage === totalPages}
|
|
|
|
|
className="px-4 py-2 rounded-[10px] border border-[#00293d]/20 font-serif text-[13px] text-[#00293d] disabled:opacity-50 disabled:cursor-not-allowed hover:border-[#00293d]/40 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Next
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-20 11:51:10 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-20 11:59:57 +05:30
|
|
|
|
|
|
|
|
{/* Filter Modal */}
|
|
|
|
|
<FilterModal
|
|
|
|
|
isOpen={isFilterModalOpen}
|
|
|
|
|
onClose={() => setIsFilterModalOpen(false)}
|
|
|
|
|
filters={filters}
|
2026-02-01 15:04:51 +05:30
|
|
|
filterFields={filterFields}
|
2026-01-20 11:59:57 +05:30
|
|
|
onToggleFilter={toggleFilter}
|
|
|
|
|
onClearAll={clearAllFilters}
|
|
|
|
|
onApply={applyFilters}
|
|
|
|
|
/>
|
2026-01-20 11:51:10 +05:30
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-25 00:21:17 +05:30
|
|
|
|
|
|
|
|
export default function ProfilesPage() {
|
|
|
|
|
return (
|
|
|
|
|
<Suspense fallback={
|
|
|
|
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#E58625]"></div>
|
|
|
|
|
<p className="text-[14px] font-serif text-[#00293D]/70">Loading...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}>
|
|
|
|
|
<ProfilesPageContent />
|
|
|
|
|
</Suspense>
|
|
|
|
|
);
|
|
|
|
|
}
|