From 911ae85e209e04bed0cb0e71076ed09cb3728b7b Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sun, 25 Jan 2026 00:21:17 +0530 Subject: [PATCH] fix --- src/app/(user)/user/profiles/page.tsx | 467 ++++++++++++++++++-------- src/services/agents.service.ts | 67 ++++ 2 files changed, 389 insertions(+), 145 deletions(-) diff --git a/src/app/(user)/user/profiles/page.tsx b/src/app/(user)/user/profiles/page.tsx index bd305f9..443a9cc 100644 --- a/src/app/(user)/user/profiles/page.tsx +++ b/src/app/(user)/user/profiles/page.tsx @@ -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(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 (
{/* Profile Image */}
- {profile.name} { + const target = e.target as HTMLImageElement; + target.src = defaultImage; + }} />
@@ -162,8 +131,10 @@ function ProfileCard({ profile }: ProfileCardProps) {
-

{profile.name}

- {profile.verified && ( +

+ {profile.firstName} {profile.lastName} +

+ {profile.isVerified && ( )}
-

{profile.title}

-
-
- {profile.matchPercentage}% Match +

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

+ {profile.rating && ( +
+ {profile.rating.toFixed(1)} Rating +
+ )}
{/* Location & Member Since */}
-
- Location - {profile.location} -
+ {profile.serviceAreas && profile.serviceAreas.length > 0 && ( +
+ Location + {profile.serviceAreas[0]} +
+ )}
- Member Since {profile.memberSince} + + Member Since {formatMemberSince(profile.createdAt)} +
{/* Bio */} -

- {showFullBio ? profile.bio : truncatedBio} - {profile.bio.length > 150 && ( - - )} -

+ {profile.bio && ( +

+ {showFullBio ? profile.bio : truncatedBio} + {profile.bio.length > 150 && ( + + )} +

+ )} - {/* Expertise */} -
-

Expertise:

-
- {profile.expertise.map((tag, index) => ( + {/* Specializations */} + {profile.specializations && profile.specializations.length > 0 && ( +
+

Expertise:

+
+ {profile.specializations.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} + + {/* Languages */} + {profile.languages && profile.languages.length > 0 && ( +
+ {profile.languages.map((lang, index) => ( - {tag} + {lang} ))}
-
- - {/* Certifications */} -
- {profile.certifications.map((cert, index) => ( - - {cert} - - ))} -
+ )}
); } -export default function ProfilesPage() { +function ProfilesPageContent() { const searchParams = useSearchParams(); + + // State for API data + const [agents, setAgents] = useState([]); + const [agentTypes, setAgentTypes] = 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 const [searchQuery, setSearchQuery] = useState(''); - const [activeTab, setActiveTab] = useState('residential'); + const [activeTypeId, setActiveTypeId] = useState(null); const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents'); const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [filters, setFilters] = useState({ @@ -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]" /> -
- {/* Category Tabs */} + {/* Agent Type Tabs */}
- {categoryTabs.map((tab) => ( + + {agentTypes.map((type) => ( ))}
+ {/* Results Count */} + {!loading && ( +

+ {totalResults} {totalResults === 1 ? 'agent' : 'agents'} found +

+ )} + + {/* 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 */} -
- {profilesData.map((profile) => ( - - ))} -
+ {!loading && !error && agents.length > 0 && ( +
+ {agents.map((profile) => ( + + ))} +
+ )} + + {/* Pagination */} + {!loading && !error && totalPages > 1 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )} @@ -421,3 +583,18 @@ export default function ProfilesPage() { ); } + +export default function ProfilesPage() { + return ( + +
+
+

Loading...

+
+ + }> + +
+ ); +} diff --git a/src/services/agents.service.ts b/src/services/agents.service.ts index f556461..81ce9e4 100644 --- a/src/services/agents.service.ts +++ b/src/services/agents.service.ts @@ -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>('/agent-types'); return response.data.data; } + + async searchAgents(params: SearchAgentsParams = {}): Promise { + 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>(url); + return response.data.data; + } } export const agentsService = new AgentsService();