299 lines
12 KiB
TypeScript
299 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Image from 'next/image';
|
|
import { useParams } from 'next/navigation';
|
|
|
|
// Import shared components
|
|
import {
|
|
InfoCard,
|
|
ExperienceSection,
|
|
ProfileCard,
|
|
SpecializationSection,
|
|
TestimonialsSection,
|
|
StatusButtons,
|
|
ContactInfo,
|
|
} from '@/components/profile';
|
|
import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agents.service';
|
|
import { getProxyImageUrl } from '@/lib/imageProxy';
|
|
import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, ExperienceData, SpecializationFieldsData } from '@/utils/profileDataMapper';
|
|
|
|
// Mock data for sections not yet available from API
|
|
const mockData = {
|
|
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,
|
|
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',
|
|
role: 'Chief Operations Officer',
|
|
rating: 5,
|
|
},
|
|
{
|
|
id: 2,
|
|
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',
|
|
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',
|
|
role: 'Chief Operations Officer',
|
|
rating: 5,
|
|
},
|
|
],
|
|
};
|
|
|
|
// Default experience data when no field values are available
|
|
const defaultExperience: ExperienceData = {
|
|
years: '-',
|
|
contracts: '-',
|
|
licensingAreas: [],
|
|
expertiseYears: [],
|
|
certifications: [],
|
|
};
|
|
|
|
// Default specialization fields data when no field values are available
|
|
const defaultSpecializationFieldsData: SpecializationFieldsData = {
|
|
fields: [],
|
|
};
|
|
|
|
export default function AgentProfileView() {
|
|
const params = useParams();
|
|
const id = params.id as string;
|
|
|
|
const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null);
|
|
const [fieldValues, setFieldValues] = useState<FieldValueResponse[]>([]);
|
|
const [experienceData, setExperienceData] = useState<ExperienceData>(defaultExperience);
|
|
const [specializationFieldsData, setSpecializationFieldsData] = useState<SpecializationFieldsData>(defaultSpecializationFieldsData);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
|
|
|
|
// Fetch agent profile and field values on mount
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
if (!id) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Fetch profile and field values in parallel
|
|
const [profile, fieldValuesResponse] = await Promise.all([
|
|
agentsService.getAgentById(id),
|
|
agentsService.getFieldValuesByAgentId(id),
|
|
]);
|
|
|
|
setAgentProfile(profile);
|
|
setFieldValues(fieldValuesResponse.fieldValues);
|
|
|
|
// Map field values to experience data structure
|
|
const mappedExperience = mapFieldValuesToExperience(fieldValuesResponse.fieldValues);
|
|
setExperienceData(mappedExperience);
|
|
|
|
// Map field values to specialization fields data (only fields from "Specialization" section)
|
|
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues);
|
|
setSpecializationFieldsData(mappedSpecializationFields);
|
|
} catch (err) {
|
|
console.error('Failed to fetch profile:', err);
|
|
setError('Failed to load profile data');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [id]);
|
|
|
|
const getProfileImageUrl = () => {
|
|
// If image failed to load, return default
|
|
if (imageError) {
|
|
return defaultImage;
|
|
}
|
|
|
|
// Check for null, undefined, or empty string
|
|
if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') {
|
|
return defaultImage;
|
|
}
|
|
|
|
// S3 URLs need to go through the backend proxy
|
|
if (agentProfile.avatar.startsWith('http')) {
|
|
return getProxyImageUrl(agentProfile.avatar);
|
|
}
|
|
|
|
// For relative paths (local assets), return as-is
|
|
return agentProfile.avatar;
|
|
};
|
|
|
|
// 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';
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6">
|
|
<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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !agentProfile) {
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6">
|
|
<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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6 space-y-6">
|
|
{/* Main Layout - Responsive: Column on mobile, Row on desktop */}
|
|
<div className="flex flex-col lg:flex-row gap-6 lg:items-stretch">
|
|
{/* Left Sidebar - Status & Contact */}
|
|
<div className="w-full lg:w-[280px] flex-shrink-0 space-y-4 flex flex-col items-center lg:items-start">
|
|
{/* Profile Image */}
|
|
<div className="relative w-[200px] lg:w-[260px]">
|
|
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={getProfileImageUrl()}
|
|
alt="Profile"
|
|
className="w-full h-full object-cover"
|
|
onError={(e) => {
|
|
const target = e.target as HTMLImageElement;
|
|
target.src = defaultImage;
|
|
setImageError(true);
|
|
}}
|
|
/>
|
|
{/* Gradient Overlay */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent pointer-events-none" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Buttons - Public view shows availability status */}
|
|
<StatusButtons isAvailable={agentProfile.isAvailable ?? true} />
|
|
|
|
{/* Contact Info */}
|
|
<ContactInfo
|
|
email={agentProfile.email || agentProfile.user?.email || ''}
|
|
phone={agentProfile.phone || ''}
|
|
/>
|
|
</div>
|
|
|
|
{/* Right Content - Profile Info + Experience + All Sections */}
|
|
<div className="flex-1 space-y-4">
|
|
{/* Profile Card - No edit button for user view */}
|
|
<ProfileCard
|
|
firstName={agentProfile.firstName}
|
|
lastName={agentProfile.lastName}
|
|
isVerified={agentProfile.isVerified}
|
|
title={agentProfile.agentType?.name || 'Real Estate Agent'}
|
|
location={agentProfile.serviceAreas?.[0] || '-'}
|
|
memberSince={formatMemberSince((agentProfile as unknown as { createdAt?: string }).createdAt)}
|
|
bio={agentProfile.bio || ''}
|
|
expertise={agentProfile.specializations || []}
|
|
showEditButton={false}
|
|
messageHref="/user/message"
|
|
/>
|
|
|
|
{/* Experience Section - Dynamic data from profile fields */}
|
|
<ExperienceSection experience={experienceData} />
|
|
|
|
{/* Info Cards Section */}
|
|
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
|
|
<InfoCard
|
|
title="Availability"
|
|
icon={
|
|
<Image
|
|
src="/assets/icons/availability-clock-icon.svg"
|
|
alt="Availability"
|
|
width={28}
|
|
height={31}
|
|
/>
|
|
}
|
|
content={
|
|
<div>
|
|
<p className="font-semibold font-serif text-[14px] leading-[19px] text-[#00293D] mb-2">{mockData.availability.type}</p>
|
|
<div className="space-y-1">
|
|
{mockData.availability.days.map((day) => (
|
|
<p key={day} className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]">{day}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
<InfoCard
|
|
title=""
|
|
icon={
|
|
<Image
|
|
src="/assets/icons/work-environment-icon.svg"
|
|
alt="Work environment"
|
|
width={28}
|
|
height={28}
|
|
/>
|
|
}
|
|
content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{mockData.preferredWorkEnvironment}</p>}
|
|
/>
|
|
<InfoCard
|
|
title=""
|
|
icon={
|
|
<Image
|
|
src="/assets/icons/testimonial-star-icon.svg"
|
|
alt="Testimonial"
|
|
width={28}
|
|
height={28}
|
|
/>
|
|
}
|
|
content={<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">“{mockData.testimonialHighlight}”</p>}
|
|
/>
|
|
</div>
|
|
|
|
{/* Specialization Section */}
|
|
<SpecializationSection fieldsData={specializationFieldsData} />
|
|
|
|
{/* Testimonials Section */}
|
|
<TestimonialsSection testimonials={mockData.testimonials} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|