fix
This commit is contained in:
@@ -1,86 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
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, clientSpecializations, loanTypes, propertyTypes, pricePoints } from '@/components/profiles/FilterModal';
|
||||
|
||||
// Sample profile data
|
||||
const profilesData = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Brian Noseland',
|
||||
verified: true,
|
||||
title: 'Licensed Real Estate Agent',
|
||||
location: 'Colorado',
|
||||
memberSince: 'March 2020',
|
||||
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
|
||||
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
|
||||
certifications: ['FHA', 'Conventional', 'USDA'],
|
||||
matchPercentage: 95,
|
||||
imageUrl: '/assets/images/professional-1.jpg',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Brian Noseland',
|
||||
verified: true,
|
||||
title: 'Licensed Real Estate Agent',
|
||||
location: 'Colorado',
|
||||
memberSince: 'March 2020',
|
||||
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
|
||||
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo'],
|
||||
certifications: ['FHA', 'Conventional', 'USDA'],
|
||||
matchPercentage: 95,
|
||||
imageUrl: '/assets/images/professional-2.jpg',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Brian Noseland',
|
||||
verified: true,
|
||||
title: 'Licensed Real Estate Agent',
|
||||
location: 'Colorado',
|
||||
memberSince: 'March 2020',
|
||||
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
|
||||
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
|
||||
certifications: ['FHA', 'Conventional', 'USDA'],
|
||||
matchPercentage: 95,
|
||||
imageUrl: '/assets/images/professional-3.jpg',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Brian Noseland',
|
||||
verified: true,
|
||||
title: 'Licensed Real Estate Agent',
|
||||
location: 'Colorado',
|
||||
memberSince: 'March 2020',
|
||||
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
|
||||
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
|
||||
certifications: ['FHA', 'Conventional', 'USDA'],
|
||||
matchPercentage: 95,
|
||||
imageUrl: '/assets/images/professional-1.jpg',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Brian Noseland',
|
||||
verified: true,
|
||||
title: 'Licensed Real Estate Agent',
|
||||
location: 'Colorado',
|
||||
memberSince: 'March 2020',
|
||||
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
|
||||
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
|
||||
certifications: ['FHA', 'Conventional', 'USDA'],
|
||||
matchPercentage: 95,
|
||||
imageUrl: '/assets/images/professional-2.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const categoryTabs = [
|
||||
{ id: 'residential', label: 'Residential' },
|
||||
{ id: 'analytics', label: 'Analytics' },
|
||||
{ id: 'commercial', label: 'Commercial' },
|
||||
{ id: 'property', label: 'Property' },
|
||||
];
|
||||
import { agentsService, AgentType, PublicAgentProfile, SearchAgentsParams } from '@/services/agents.service';
|
||||
import { uploadService } from '@/services/upload.service';
|
||||
|
||||
interface FilterSectionProps {
|
||||
title: string;
|
||||
@@ -130,23 +56,66 @@ function FilterSection({ title, options, selectedOptions, onToggle, showMore }:
|
||||
}
|
||||
|
||||
interface ProfileCardProps {
|
||||
profile: typeof profilesData[0];
|
||||
profile: PublicAgentProfile;
|
||||
}
|
||||
|
||||
function ProfileCard({ profile }: ProfileCardProps) {
|
||||
const [showFullBio, setShowFullBio] = useState(false);
|
||||
const truncatedBio = profile.bio.length > 150 ? profile.bio.slice(0, 150) + '...' : profile.bio;
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<Image
|
||||
src={profile.imageUrl}
|
||||
alt={profile.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
{/* 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;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Link href={`/user/profile/${profile.id}`}>
|
||||
@@ -162,8 +131,10 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-fractul font-bold text-[16px] text-[#00293d]">{profile.name}</h3>
|
||||
{profile.verified && (
|
||||
<h3 className="font-fractul font-bold text-[16px] text-[#00293d]">
|
||||
{profile.firstName} {profile.lastName}
|
||||
</h3>
|
||||
{profile.isVerified && (
|
||||
<span className="flex items-center gap-1 text-[#e58625] font-serif text-[12px]">
|
||||
<Image
|
||||
src="/assets/icons/verified-badge-blue.svg"
|
||||
@@ -175,15 +146,20 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="font-serif text-[13px] text-[#00293d] mt-0.5">{profile.title}</p>
|
||||
<p className="font-serif text-[13px] text-[#00293d] mt-0.5">
|
||||
{profile.agentType?.name || 'Real Estate Professional'}
|
||||
</p>
|
||||
</div>
|
||||
{profile.rating && (
|
||||
<div className="bg-[#e58625] text-white px-3 py-1 rounded-full font-fractul font-bold text-[12px]">
|
||||
{profile.matchPercentage}% Match
|
||||
{profile.rating.toFixed(1)} Rating
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Location & Member Since */}
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
{profile.serviceAreas && profile.serviceAreas.length > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Image
|
||||
src="/assets/icons/location-pin-orange.svg"
|
||||
@@ -191,8 +167,9 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
||||
width={12}
|
||||
height={15}
|
||||
/>
|
||||
<span className="font-serif text-[12px] text-[#00293d]">{profile.location}</span>
|
||||
<span className="font-serif text-[12px] text-[#00293d]">{profile.serviceAreas[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Image
|
||||
src="/assets/icons/calendar-icon.svg"
|
||||
@@ -200,11 +177,14 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
<span className="font-serif text-[12px] text-[#00293d]">Member Since {profile.memberSince}</span>
|
||||
<span className="font-serif text-[12px] text-[#00293d]">
|
||||
Member Since {formatMemberSince(profile.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
{profile.bio && (
|
||||
<p className="font-serif text-[13px] text-[#00293d] mt-3 leading-relaxed">
|
||||
{showFullBio ? profile.bio : truncatedBio}
|
||||
{profile.bio.length > 150 && (
|
||||
@@ -216,12 +196,14 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Expertise */}
|
||||
{/* 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.expertise.map((tag, index) => (
|
||||
{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]"
|
||||
@@ -231,27 +213,41 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Certifications */}
|
||||
{/* Languages */}
|
||||
{profile.languages && profile.languages.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||
{profile.certifications.map((cert, index) => (
|
||||
{profile.languages.map((lang, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="border border-[#00293d]/30 rounded-full px-3 py-1 font-serif text-[11px] text-[#00293d]"
|
||||
>
|
||||
{cert}
|
||||
{lang}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilesPage() {
|
||||
function ProfilesPageContent() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// State for API data
|
||||
const [agents, setAgents] = useState<PublicAgentProfile[]>([]);
|
||||
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
||||
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);
|
||||
|
||||
// State for search and filters
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState('residential');
|
||||
const [activeTypeId, setActiveTypeId] = useState<string | null>(null);
|
||||
const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents');
|
||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
@@ -263,6 +259,77 @@ export default function ProfilesPage() {
|
||||
popularFilters: [],
|
||||
});
|
||||
|
||||
// Initialize from URL params
|
||||
useEffect(() => {
|
||||
const type = searchParams.get('type');
|
||||
const name = searchParams.get('name');
|
||||
const location = searchParams.get('location');
|
||||
|
||||
if (type) setActiveTypeId(type);
|
||||
if (name) setSearchQuery(name);
|
||||
// Location is handled separately in search params
|
||||
}, [searchParams]);
|
||||
|
||||
// Fetch agent types on mount
|
||||
useEffect(() => {
|
||||
const fetchAgentTypes = async () => {
|
||||
try {
|
||||
const types = await agentsService.getAgentTypes();
|
||||
setAgentTypes(types);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch agent types:', error);
|
||||
}
|
||||
};
|
||||
fetchAgentTypes();
|
||||
}, []);
|
||||
|
||||
// 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]);
|
||||
|
||||
// Fetch agents when filters change
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
|
||||
const toggleFilter = (category: string, option: string) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
@@ -285,7 +352,18 @@ export default function ProfilesPage() {
|
||||
|
||||
const applyFilters = () => {
|
||||
setIsFilterModalOpen(false);
|
||||
// Apply filter logic here
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -372,9 +450,13 @@ export default function ProfilesPage() {
|
||||
placeholder="Search Agents"
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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]"
|
||||
/>
|
||||
<button className="absolute right-2 top-1/2 -translate-y-1/2 w-[36px] h-[36px] bg-[#e58625] rounded-[8px] flex items-center justify-center">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
@@ -382,29 +464,109 @@ export default function ProfilesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
{/* Agent Type Tabs */}
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{categoryTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
onClick={() => handleTypeChange(null)}
|
||||
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${
|
||||
activeTab === tab.id
|
||||
activeTypeId === null
|
||||
? 'bg-[#00293d] text-white border-[#00293d]'
|
||||
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
All Types
|
||||
</button>
|
||||
{agentTypes.map((type) => (
|
||||
<button
|
||||
key={type.id}
|
||||
onClick={() => handleTypeChange(type.id)}
|
||||
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${
|
||||
activeTypeId === type.id
|
||||
? 'bg-[#00293d] text-white border-[#00293d]'
|
||||
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
|
||||
}`}
|
||||
>
|
||||
{type.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Profile Cards */}
|
||||
{!loading && !error && agents.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{profilesData.map((profile) => (
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -421,3 +583,18 @@ export default function ProfilesPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,52 @@ export interface SaveFieldValuesResponse {
|
||||
data: unknown[];
|
||||
}
|
||||
|
||||
// Search parameters for public agent search
|
||||
export interface SearchAgentsParams {
|
||||
search?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
agentTypeId?: string;
|
||||
isVerified?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: 'createdAt' | 'averageRating' | 'totalReviews' | 'firstName';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// Public agent listing response
|
||||
export interface PublicAgentProfile {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
bio: string | null;
|
||||
avatar: string | null;
|
||||
licenseNumber: string | null;
|
||||
experience: number | null;
|
||||
specializations: string[];
|
||||
languages: string[];
|
||||
serviceAreas: string[];
|
||||
rating: number | null;
|
||||
reviewCount: number;
|
||||
isVerified: boolean;
|
||||
isFeatured: boolean;
|
||||
agentTypeId: string | null;
|
||||
agentType: AgentType | null;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface SearchAgentsResponse {
|
||||
data: PublicAgentProfile[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// Agents Service for Web
|
||||
class AgentsService {
|
||||
private basePath = '/agents';
|
||||
@@ -113,6 +159,27 @@ class AgentsService {
|
||||
const response = await api.get<ApiResponse<AgentType[]>>('/agent-types');
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async searchAgents(params: SearchAgentsParams = {}): Promise<SearchAgentsResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params.search) queryParams.set('search', params.search);
|
||||
if (params.city) queryParams.set('city', params.city);
|
||||
if (params.state) queryParams.set('state', params.state);
|
||||
if (params.country) queryParams.set('country', params.country);
|
||||
if (params.agentTypeId) queryParams.set('agentTypeId', params.agentTypeId);
|
||||
if (params.isVerified !== undefined) queryParams.set('isVerified', String(params.isVerified));
|
||||
if (params.page) queryParams.set('page', String(params.page));
|
||||
if (params.limit) queryParams.set('limit', String(params.limit));
|
||||
if (params.sortBy) queryParams.set('sortBy', params.sortBy);
|
||||
if (params.sortOrder) queryParams.set('sortOrder', params.sortOrder);
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = queryString ? `${this.basePath}?${queryString}` : this.basePath;
|
||||
|
||||
const response = await api.get<ApiResponse<SearchAgentsResponse>>(url);
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const agentsService = new AgentsService();
|
||||
|
||||
Reference in New Issue
Block a user