feat: Implement agent-user connection request system with a new service and dynamic profile card buttons.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user