feat: Implement agent-user connection request system with a new service and dynamic profile card buttons.

This commit is contained in:
pradeepkumar
2026-02-02 09:59:08 +05:30
parent edfbf4a51c
commit b5106945dc
10 changed files with 1018 additions and 151 deletions

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { useParams } from 'next/navigation';
import { useSession } from 'next-auth/react';
// Import shared components
import {
@@ -14,9 +15,11 @@ import {
StatusButtons,
ContactInfo,
} from '@/components/profile';
import { ConnectRequestModal } from '@/components/modals';
import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agents.service';
import { getProxyImageUrl } from '@/lib/imageProxy';
import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, ExperienceData, SpecializationFieldsData } from '@/utils/profileDataMapper';
import { connectionRequestsService, ConnectionStatus } from '@/services/connection-requests.service';
import { uploadService } from '@/services/upload.service';
import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, mapFieldValuesToProfileCard, ExperienceData, SpecializationFieldsData, ProfileCardData } from '@/utils/profileDataMapper';
// Mock data for sections not yet available from API
const mockData = {
@@ -65,20 +68,44 @@ const defaultSpecializationFieldsData: SpecializationFieldsData = {
fields: [],
};
// Default profile card data when no field values are available
const defaultProfileCardData: ProfileCardData = {
bio: '',
expertise: [],
serviceAreas: [],
city: null,
state: null,
};
export default function AgentProfileView() {
const params = useParams();
const id = params.id as string;
const { data: session } = useSession();
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 [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [imageError, setImageError] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
// Connect modal state
const [showConnectModal, setShowConnectModal] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus | null>(null);
const [connectionRequestId, setConnectionRequestId] = useState<string | null>(null);
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
// 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
useEffect(() => {
const fetchData = async () => {
@@ -104,6 +131,24 @@ export default function AgentProfileView() {
// Map field values to specialization fields data (only fields from "Specialization" section)
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues);
setSpecializationFieldsData(mappedSpecializationFields);
// Map field values to profile card data (bio, expertise, location)
const mappedProfileCard = mapFieldValuesToProfileCard(fieldValuesResponse.fieldValues);
setProfileCardData(mappedProfileCard);
// 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);
}
} catch (err) {
console.error('Failed to fetch profile:', err);
setError('Failed to load profile data');
@@ -115,24 +160,65 @@ export default function AgentProfileView() {
fetchData();
}, [id]);
// Fetch connection status when user is logged in
useEffect(() => {
const fetchConnectionStatus = async () => {
if (!id || !session) return;
try {
const statusResponse = await connectionRequestsService.getConnectionStatus(id);
setConnectionStatus(statusResponse?.status || null);
setConnectionRequestId(statusResponse?.id || null);
} catch (err) {
console.error('Failed to fetch connection status:', err);
// Non-critical error, don't show to user
}
};
fetchConnectionStatus();
}, [id, session]);
// Handle connection request success
const handleConnectionSuccess = () => {
setConnectionStatus('PENDING');
};
// Handle unlink/disconnect
const handleUnlink = async () => {
if (!connectionRequestId) return;
try {
await connectionRequestsService.cancelRequest(connectionRequestId);
setConnectionStatus(null);
setConnectionRequestId(null);
} catch (err) {
console.error('Failed to unlink connection:', err);
}
};
const getProfileImageUrl = () => {
// If image failed to load, return default
if (imageError) {
return defaultImage;
}
// If we have a presigned URL from S3, use it
if (avatarUrl) {
return avatarUrl;
}
// 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
if (agentProfile.avatar.startsWith('/')) {
return agentProfile.avatar;
}
// For relative paths (local assets), return as-is
return agentProfile.avatar;
// Default fallback
return defaultImage;
};
// Format member since date
@@ -208,8 +294,13 @@ export default function AgentProfileView() {
</div>
</div>
{/* Status Buttons - Public view shows availability status */}
<StatusButtons isAvailable={agentProfile.isAvailable ?? true} />
{/* Status Buttons - Public view shows availability status with Connect/Pending/Unlink button */}
<StatusButtons
isAvailable={agentProfile.isAvailable ?? true}
connectionStatus={connectionStatus}
onConnectClick={() => setShowConnectModal(true)}
onUnlinkClick={handleUnlink}
/>
{/* Contact Info */}
<ContactInfo
@@ -226,12 +317,17 @@ export default function AgentProfileView() {
lastName={agentProfile.lastName}
isVerified={agentProfile.isVerified}
title={agentProfile.agentType?.name || 'Real Estate Agent'}
location={agentProfile.serviceAreas?.[0] || '-'}
location={profileCardData.city && profileCardData.state
? `${profileCardData.city}, ${profileCardData.state}`
: profileCardData.city || profileCardData.state || agentProfile.serviceAreas?.[0] || '-'}
memberSince={formatMemberSince((agentProfile as unknown as { createdAt?: string }).createdAt)}
bio={agentProfile.bio || ''}
expertise={agentProfile.specializations || []}
bio={profileCardData.bio || agentProfile.bio || ''}
expertise={profileCardData.expertise.length > 0 ? profileCardData.expertise : (agentProfile.specializations || [])}
showEditButton={false}
messageHref="/user/message"
connectionStatus={connectionStatus}
onConnectClick={() => setShowConnectModal(true)}
onUnlinkClick={handleUnlink}
/>
{/* Experience Section - Dynamic data from profile fields */}
@@ -293,6 +389,16 @@ export default function AgentProfileView() {
<TestimonialsSection testimonials={mockData.testimonials} />
</div>
</div>
{/* Connect Request Modal */}
<ConnectRequestModal
isOpen={showConnectModal}
onClose={() => setShowConnectModal(false)}
agentProfileId={id}
agentName={`${agentProfile.firstName || ''} ${agentProfile.lastName || ''}`.trim() || 'Agent'}
existingStatus={connectionStatus}
onSuccess={handleConnectionSuccess}
/>
</div>
);
}