2026-01-11 21:30:57 +05:30
'use client' ;
2026-01-24 22:19:30 +05:30
import { useState , useEffect } from 'react' ;
2026-01-11 21:30:57 +05:30
import { useSession } from 'next-auth/react' ;
2026-01-11 22:09:41 +05:30
import Image from 'next/image' ;
2026-01-12 12:34:58 +05:30
2026-01-20 12:17:47 +05:30
// Import shared components
2026-01-12 12:34:58 +05:30
import {
InfoCard ,
2026-01-18 18:16:55 +05:30
ExperienceSection ,
2026-01-18 19:39:46 +05:30
ProfileCard ,
SpecializationSection ,
TestimonialsSection ,
2026-01-18 19:44:24 +05:30
StatusButtons ,
ContactInfo ,
2026-01-24 22:19:30 +05:30
PhotoUploadModal ,
2026-01-20 12:17:47 +05:30
} from '@/components/profile' ;
2026-01-24 23:10:02 +05:30
import { agentsService , AgentProfile , FieldValueResponse } from '@/services/agents.service' ;
2026-01-24 22:19:30 +05:30
import { uploadService } from '@/services/upload.service' ;
2026-01-25 01:15:02 +05:30
import { mapFieldValuesToExperience , mapFieldValuesToProfileCard , mapFieldValuesToAvailability , mapFieldValuesToSpecializationFields , ExperienceData , ProfileCardData , AvailabilityData , SpecializationFieldsData } from '@/utils/profileDataMapper' ;
2026-01-11 22:09:41 +05:30
2026-01-24 22:19:30 +05:30
// Mock data for sections not yet available from API
const mockData = {
2026-01-11 22:09:41 +05:30
experience : {
years : '10+ years' ,
contracts : '10+ Contracts' ,
licensingAreas : [ 'Colorado Springs' , 'Monument' , 'Falcon' , 'Castle Rock' , 'Fountain' , 'Momentum' ] ,
expertiseYears : [
{ area : 'Colorado' , years : '10 yrs' } ,
{ area : 'Falcon' , years : '10 yrs' } ,
{ area : 'Colorado' , years : '10 yrs' } ,
{ area : 'Colorado' , years : '10 yrs' } ,
] ,
certifications : [
{ name : 'Certified Residential Specialist (CRS)' , org : 'RESIDENTIAL REAL ESTATE COUNCIL' } ,
{ name : 'Certified Residential Specialist (CRS)' , org : 'RESIDENTIAL REAL ESTATE COUNCIL' } ,
] ,
} ,
availability : {
type : 'Full-time' ,
days : [ 'Monday' , 'Tuesday' , 'Wednesday' , 'Thursday' , 'Friday' , 'Saturday' ] ,
} ,
preferredWorkEnvironment : 'Preferred work environment includes on-site property visits, in-depth market research, client consultations, property tours, and active involvement in negotiations to ensure the best outcomes' ,
testimonialHighlight : "The most amazing experience I've had as a real estate professional is helping a family secure their dream home and seeing their happiness when they received the keys." ,
testimonials : [
{
id : 1 ,
2026-01-12 14:13:37 +05:30
text : 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.' ,
2026-01-18 21:08:28 +05:30
author : 'Kenedy Kenney' ,
2026-01-11 22:09:41 +05:30
role : 'Chief Operations Officer' ,
rating : 5 ,
} ,
{
id : 2 ,
2026-01-12 14:13:37 +05:30
text : 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.' ,
2026-01-18 21:08:28 +05:30
author : 'Kenedy Kenney' ,
role : 'Chief Operations Officer' ,
rating : 5 ,
} ,
{
id : 3 ,
text : 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.' ,
author : 'Kenedy Kenney' ,
2026-01-11 22:09:41 +05:30
role : 'Chief Operations Officer' ,
rating : 5 ,
} ,
] ,
} ;
2026-01-24 23:10:02 +05:30
// Default experience data when no field values are available
const defaultExperience : ExperienceData = {
years : '-' ,
contracts : '-' ,
licensingAreas : [ ] ,
expertiseYears : [ ] ,
certifications : [ ] ,
} ;
2026-01-24 23:31:32 +05:30
// Default profile card data when no field values are available
const defaultProfileCardData : ProfileCardData = {
bio : '' ,
expertise : [ ] ,
serviceAreas : [ ] ,
} ;
// Default availability data when no field values are available
const defaultAvailabilityData : AvailabilityData = {
type : '' ,
schedule : [ ] ,
} ;
2026-01-25 01:15:02 +05:30
// Default specialization fields data when no field values are available
const defaultSpecializationFieldsData : SpecializationFieldsData = {
fields : [ ] ,
} ;
2026-01-11 21:30:57 +05:30
export default function AgentDashboard() {
const { data : session } = useSession ( ) ;
2026-01-24 22:19:30 +05:30
const [ agentProfile , setAgentProfile ] = useState < AgentProfile | null > ( null ) ;
2026-01-24 23:10:02 +05:30
const [ fieldValues , setFieldValues ] = useState < FieldValueResponse [ ] > ( [ ] ) ;
const [ experienceData , setExperienceData ] = useState < ExperienceData > ( defaultExperience ) ;
2026-01-24 23:31:32 +05:30
const [ profileCardData , setProfileCardData ] = useState < ProfileCardData > ( defaultProfileCardData ) ;
const [ availabilityData , setAvailabilityData ] = useState < AvailabilityData > ( defaultAvailabilityData ) ;
2026-01-25 01:15:02 +05:30
const [ specializationFieldsData , setSpecializationFieldsData ] = useState < SpecializationFieldsData > ( defaultSpecializationFieldsData ) ;
2026-01-24 22:19:30 +05:30
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
const [ isPhotoModalOpen , setIsPhotoModalOpen ] = useState ( false ) ;
const [ imageError , setImageError ] = useState ( false ) ;
2026-01-24 23:10:02 +05:30
const [ avatarUrl , setAvatarUrl ] = useState < string | null > ( null ) ;
2026-01-24 22:19:30 +05:30
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg' ;
2026-01-24 23:10:02 +05:30
// Helper to check if avatar is an S3 key (not a full URL or local path)
const isS3Key = ( avatar : string | null | undefined ) : boolean = > {
if ( ! avatar ) return false ;
// S3 keys don't start with http or /
return ! avatar . startsWith ( 'http' ) && ! avatar . startsWith ( '/' ) ;
} ;
// Fetch agent profile and field values on mount
2026-01-24 22:19:30 +05:30
useEffect ( ( ) = > {
2026-01-24 23:10:02 +05:30
const fetchData = async ( ) = > {
2026-01-24 22:19:30 +05:30
try {
setLoading ( true ) ;
setError ( null ) ;
2026-01-24 23:10:02 +05:30
// Fetch profile and field values in parallel
const [ profile , fieldValuesResponse ] = await Promise . all ( [
agentsService . getMyProfile ( ) ,
agentsService . getFieldValues ( ) ,
] ) ;
2026-01-24 22:19:30 +05:30
setAgentProfile ( profile ) ;
2026-01-24 23:10:02 +05:30
setFieldValues ( fieldValuesResponse . fieldValues ) ;
// Map field values to experience data structure
const mappedExperience = mapFieldValuesToExperience ( fieldValuesResponse . fieldValues ) ;
setExperienceData ( mappedExperience ) ;
2026-01-24 23:31:32 +05:30
// Map field values to profile card data (bio, expertise)
const mappedProfileCard = mapFieldValuesToProfileCard ( fieldValuesResponse . fieldValues ) ;
setProfileCardData ( mappedProfileCard ) ;
// Map field values to availability data
const mappedAvailability = mapFieldValuesToAvailability ( fieldValuesResponse . fieldValues ) ;
setAvailabilityData ( mappedAvailability ) ;
2026-01-25 01:15:02 +05:30
// Map field values to specialization fields data (only fields from "Specialization" section)
const mappedSpecializationFields = mapFieldValuesToSpecializationFields ( fieldValuesResponse . fieldValues ) ;
setSpecializationFieldsData ( mappedSpecializationFields ) ;
2026-01-24 23:10:02 +05:30
// If avatar is an S3 key, fetch presigned URL
if ( profile . avatar && isS3Key ( profile . avatar ) ) {
try {
const presignedUrl = await uploadService . getPresignedDownloadUrl ( profile . avatar ) ;
setAvatarUrl ( presignedUrl ) ;
} catch ( avatarErr ) {
console . error ( 'Failed to get avatar URL:' , avatarErr ) ;
// Don't fail the whole page, just use default image
}
} else if ( profile . avatar ) {
// It's already a full URL or local path
setAvatarUrl ( profile . avatar ) ;
}
2026-01-24 22:19:30 +05:30
} catch ( err ) {
console . error ( 'Failed to fetch profile:' , err ) ;
setError ( 'Failed to load profile data' ) ;
} finally {
setLoading ( false ) ;
}
} ;
2026-01-24 23:10:02 +05:30
fetchData ( ) ;
2026-01-24 22:19:30 +05:30
} , [ ] ) ;
const handlePhotoUpload = async ( file : File ) = > {
// Upload avatar to S3 (uses agent ID as filename for auto-replacement)
const uploadedFile = await uploadService . uploadAvatar ( file ) ;
2026-01-24 23:10:02 +05:30
// Update agent profile with the S3 key
2026-01-24 22:19:30 +05:30
const updatedProfile = await agentsService . updateProfile ( { avatar : uploadedFile.url } ) ;
setImageError ( false ) ; // Reset error state for new image
setAgentProfile ( updatedProfile ) ;
2026-01-24 23:10:02 +05:30
// Fetch new presigned URL for the uploaded avatar
if ( uploadedFile . url && isS3Key ( uploadedFile . url ) ) {
try {
const presignedUrl = await uploadService . getPresignedDownloadUrl ( uploadedFile . url ) ;
setAvatarUrl ( presignedUrl ) ;
} catch ( err ) {
console . error ( 'Failed to get presigned URL for new avatar:' , err ) ;
}
}
2026-01-24 22:19:30 +05:30
} ;
const getProfileImageUrl = ( ) = > {
// If image failed to load, return default
if ( imageError ) {
return defaultImage ;
}
2026-01-24 23:10:02 +05:30
// If we have a presigned URL from S3, use it
if ( avatarUrl ) {
return avatarUrl ;
}
2026-01-24 22:19:30 +05:30
// Check for null, undefined, or empty string
if ( ! agentProfile ? . avatar || agentProfile . avatar . trim ( ) === '' ) {
return defaultImage ;
}
2026-01-24 23:10:02 +05:30
// For relative paths (local assets), return as-is
if ( agentProfile . avatar . startsWith ( '/' ) ) {
2026-01-24 22:19:30 +05:30
return agentProfile . avatar ;
}
2026-01-24 23:10:02 +05:30
// Default fallback
return defaultImage ;
2026-01-24 22:19:30 +05:30
} ;
if ( loading ) {
return (
< div className = "flex items-center justify-center h-[calc(100vh-180px)]" >
< 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 profile . . . < / p >
< / div >
< / div >
) ;
}
if ( error || ! agentProfile ) {
return (
< div className = "flex items-center justify-center h-[calc(100vh-180px)]" >
< div className = "bg-white rounded-[20px] p-8 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] max-w-md text-center" >
< div className = "w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4" >
< svg className = "w-8 h-8 text-red-500" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< / div >
< h2 className = "text-[18px] font-bold font-serif text-[#00293D] mb-2" > Unable to Load Profile < / h2 >
< p className = "text-[14px] font-serif text-[#00293D]/70 mb-4" > { error || 'Profile not found' } < / p >
< button
onClick = { ( ) = > window . location . reload ( ) }
className = "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 >
< / div >
) ;
}
// 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-11 21:30:57 +05:30
return (
< div className = "space-y-6" >
2026-01-18 19:27:13 +05:30
{ /* Main Layout - Responsive: Column on mobile, Row on desktop */ }
< div className = "flex flex-col lg:flex-row gap-6 lg:items-stretch" >
2026-01-11 22:09:41 +05:30
{ /* Left Sidebar - Status & Contact */ }
2026-01-18 19:27:13 +05:30
< div className = "w-full lg:w-[280px] flex-shrink-0 space-y-4 flex flex-col items-center lg:items-start" >
2026-01-12 14:13:37 +05:30
{ /* Profile Image */ }
2026-01-18 19:27:13 +05:30
< div className = "relative w-[200px] lg:w-[260px]" >
< div className = "w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden" >
2026-01-24 22:19:30 +05:30
{ /* eslint-disable-next-line @next/next/no-img-element */ }
< img
src = { getProfileImageUrl ( ) }
2026-01-12 14:13:37 +05:30
alt = "Profile"
className = "w-full h-full object-cover"
2026-01-24 22:19:30 +05:30
onError = { ( e ) = > {
const target = e . target as HTMLImageElement ;
target . src = defaultImage ;
} }
2026-01-12 14:13:37 +05:30
/ >
2026-01-18 08:44:53 +05:30
{ /* Gradient Overlay */ }
< div className = "absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent pointer-events-none" / >
2026-01-12 14:13:37 +05:30
< / div >
2026-01-18 08:44:53 +05:30
{ /* Edit Icon - Top Right Outside */ }
2026-01-24 22:19:30 +05:30
< button
onClick = { ( ) = > setIsPhotoModalOpen ( true ) }
className = "absolute -top-3 -right-3 w-9 h-9 bg-white rounded-[10px] shadow-md flex items-center justify-center hover:bg-gray-50 transition-colors"
>
2026-01-12 14:13:37 +05:30
< Image
src = "/assets/icons/edit-icon.svg"
2026-01-18 21:29:52 +05:30
alt = "Edit profile image"
2026-01-12 14:13:37 +05:30
width = { 20 }
height = { 20 }
/ >
2026-01-18 21:29:52 +05:30
< / button >
2026-01-12 14:13:37 +05:30
< / div >
{ /* Status Buttons */ }
2026-01-18 19:44:24 +05:30
< StatusButtons / >
2026-01-12 14:13:37 +05:30
{ /* Contact Info */ }
2026-01-24 22:19:30 +05:30
< ContactInfo
email = { agentProfile . email || agentProfile . user ? . email || '' }
phone = { agentProfile . phone || '' }
/ >
2026-01-12 14:13:37 +05:30
< / div >
2026-01-11 21:30:57 +05:30
2026-01-18 19:27:13 +05:30
{ /* Right Content - Profile Info + Experience + All Sections */ }
2026-01-18 20:18:30 +05:30
< div className = "flex-1 space-y-4" >
2026-01-18 18:16:55 +05:30
{ /* Profile Card */ }
2026-01-18 19:39:46 +05:30
< ProfileCard
2026-01-24 22:19:30 +05:30
firstName = { agentProfile . firstName }
lastName = { agentProfile . lastName }
isVerified = { agentProfile . isVerified }
title = { agentProfile . agentType ? . name || 'Real Estate Agent' }
2026-01-24 23:31:32 +05:30
location = { profileCardData . serviceAreas ? . [ 0 ] || ( agentProfile as unknown as { city? : string } ) . city || 'United States' }
2026-01-24 22:19:30 +05:30
memberSince = { formatMemberSince ( ( agentProfile as unknown as { createdAt? : string } ) . createdAt ) }
2026-01-24 23:31:32 +05:30
bio = { profileCardData . bio || agentProfile . bio || '' }
expertise = { profileCardData . expertise }
2026-01-20 12:17:47 +05:30
showEditButton = { true }
editHref = "/agent/edit"
messageHref = "/agent/message"
requestsHref = "/agent/network"
2026-01-18 19:39:46 +05:30
/ >
2026-01-11 21:30:57 +05:30
2026-01-24 23:10:02 +05:30
{ /* Experience Section - Dynamic data from profile fields */ }
< ExperienceSection experience = { experienceData } / >
2026-01-18 18:16:55 +05:30
2026-01-18 19:27:13 +05:30
{ /* Info Cards Section */ }
< div className = "flex flex-col lg:grid lg:grid-cols-3 gap-6" >
2026-01-18 18:16:55 +05:30
< InfoCard
title = "Availability"
icon = {
< Image
2026-01-18 20:28:09 +05:30
src = "/assets/icons/availability-clock-icon.svg"
2026-01-18 18:16:55 +05:30
alt = "Availability"
2026-01-18 20:28:09 +05:30
width = { 28 }
height = { 31 }
2026-01-18 18:16:55 +05:30
/ >
}
content = {
< div >
2026-01-24 23:31:32 +05:30
{ availabilityData . type && (
< p className = "font-semibold font-serif text-[14px] leading-[19px] text-[#00293D] mb-2" > { availabilityData . type } < / p >
) }
2026-01-18 18:16:55 +05:30
< div className = "space-y-1" >
2026-01-24 23:31:32 +05:30
{ availabilityData . schedule . length > 0 ? (
availabilityData . schedule . map ( ( item , index ) = > (
< p key = { index } className = "font-normal font-serif text-[14px] leading-[19px] text-[#00293D]" > { item } < / p >
) )
) : (
< p className = "font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60" > Not specified < / p >
) }
2026-01-18 18:16:55 +05:30
< / div >
< / div >
}
/ >
< InfoCard
2026-01-18 20:28:09 +05:30
title = ""
2026-01-18 18:16:55 +05:30
icon = {
< Image
2026-01-18 20:28:09 +05:30
src = "/assets/icons/work-environment-icon.svg"
2026-01-18 18:16:55 +05:30
alt = "Work environment"
2026-01-18 20:28:09 +05:30
width = { 28 }
height = { 28 }
2026-01-18 18:16:55 +05:30
/ >
}
2026-01-24 22:19:30 +05:30
content = { < p className = "font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]" > { mockData . preferredWorkEnvironment } < / p > }
2026-01-18 18:16:55 +05:30
/ >
< InfoCard
title = ""
icon = {
< Image
2026-01-18 20:28:09 +05:30
src = "/assets/icons/testimonial-star-icon.svg"
2026-01-18 18:16:55 +05:30
alt = "Testimonial"
2026-01-18 20:28:09 +05:30
width = { 28 }
height = { 28 }
2026-01-18 18:16:55 +05:30
/ >
}
2026-01-24 22:19:30 +05:30
content = { < p className = "text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]" > & ldquo ; { mockData . testimonialHighlight } & rdquo ; < / p > }
2026-01-18 18:16:55 +05:30
/ >
2026-01-12 14:13:37 +05:30
< / div >
2026-01-18 19:27:13 +05:30
{ /* Specialization Section */ }
2026-01-25 01:15:02 +05:30
< SpecializationSection fieldsData = { specializationFieldsData } / >
2026-01-18 18:16:55 +05:30
2026-01-18 19:27:13 +05:30
{ /* Testimonials Section */ }
2026-01-24 22:19:30 +05:30
< TestimonialsSection testimonials = { mockData . testimonials } / >
2026-01-12 14:13:37 +05:30
< / div >
< / div >
2026-01-24 22:19:30 +05:30
{ /* Photo Upload Modal */ }
< PhotoUploadModal
isOpen = { isPhotoModalOpen }
onClose = { ( ) = > setIsPhotoModalOpen ( false ) }
onUpload = { handlePhotoUpload }
currentPhoto = { agentProfile . avatar }
2026-01-24 23:10:02 +05:30
currentPhotoUrl = { avatarUrl }
2026-01-24 22:19:30 +05:30
/ >
2026-01-11 21:30:57 +05:30
< / div >
) ;
}