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

@@ -0,0 +1,79 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
interface ConnectionCardProps {
id: string;
name: string;
avatar: string;
email: string;
connectedAt?: string;
onMessage?: (id: string) => void;
}
// Format date
function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
export function ConnectionCard({
id,
name,
avatar,
email,
connectedAt,
onMessage,
}: ConnectionCardProps) {
return (
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4">
{/* Avatar */}
<div className="flex-shrink-0">
<Image
src={avatar}
alt={name}
width={80}
height={80}
className="rounded-full object-cover w-[80px] h-[80px]"
/>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
{/* Name */}
<h3 className="font-serif font-bold text-[14px] leading-[19px] text-[#00293D] mb-1">
{name}
</h3>
{/* Email */}
<p className="font-serif font-normal text-[13px] leading-[18px] text-[#00293D]/70 mb-1">
{email}
</p>
{/* Connected date */}
{connectedAt && (
<p className="font-serif font-normal text-[12px] text-[#00293D]/50">
Connected on {formatDate(connectedAt)}
</p>
)}
</div>
{/* Action Buttons */}
<div className="flex items-center gap-3 flex-shrink-0">
<button
onClick={() => onMessage?.(id)}
className="h-[27px] px-4 rounded-[15px] bg-[#e58625] font-fractul font-medium text-[14px] text-[#00293D] hover:bg-[#d47920] transition-colors cursor-pointer flex items-center gap-2"
>
<Image
src="/assets/icons/message-dark-icon.svg"
alt="Message"
width={14}
height={13}
/>
Message
</button>
</div>
</div>
);
}

View File

@@ -7,22 +7,41 @@ interface InvitationCardProps {
name: string;
avatar: string;
description: string;
mutualConnection?: string;
createdAt?: string;
isProcessing?: boolean;
onAccept?: (id: string) => void;
onIgnore?: (id: string) => void;
}
// Format relative time
function formatRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
export function InvitationCard({
id,
name,
avatar,
description,
mutualConnection,
createdAt,
isProcessing = false,
onAccept,
onIgnore,
}: InvitationCardProps) {
return (
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4">
<div className={`flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4 ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}>
{/* Avatar */}
<div className="flex-shrink-0">
<Image
@@ -36,53 +55,39 @@ export function InvitationCard({
{/* Content */}
<div className="flex-1 min-w-0">
{/* Name with verified icon */}
{/* Name with time */}
<div className="flex items-center gap-2 mb-1">
<h3 className="font-serif font-bold text-[14px] leading-[19px] text-[#00293D]">
{name}
</h3>
<Image
src="/assets/icons/shield-verified-icon.svg"
alt="Verified"
width={19}
height={19}
/>
{createdAt && (
<span className="font-serif font-normal text-[12px] text-[#00293D]/50">
{formatRelativeTime(createdAt)}
</span>
)}
</div>
{/* Description */}
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] mb-1">
{/* Description/Message */}
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] line-clamp-2">
{description}
</p>
{/* Mutual Connection */}
{mutualConnection && (
<div className="flex items-center gap-2">
<Image
src="/assets/icons/chain-icon.svg"
alt="Connection"
width={18}
height={18}
/>
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D]">
Mutual Connection With {mutualConnection}
</span>
</div>
)}
</div>
{/* Action Buttons */}
<div className="flex items-center gap-3 flex-shrink-0">
<button
onClick={() => onIgnore?.(id)}
className="h-[27px] px-4 rounded-[15px] border border-[#00293d] font-serif font-light text-[14px] text-[#00293D] hover:bg-[#00293d]/5 transition-colors cursor-pointer"
disabled={isProcessing}
className="h-[27px] px-4 rounded-[15px] border border-[#00293d] font-serif font-light text-[14px] text-[#00293D] hover:bg-[#00293d]/5 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
Ignore
</button>
<button
onClick={() => onAccept?.(id)}
className="h-[27px] px-4 rounded-[15px] bg-[#e58625] font-fractul font-medium text-[14px] text-[#00293D] hover:bg-[#d47920] transition-colors cursor-pointer"
disabled={isProcessing}
className="h-[27px] px-4 rounded-[15px] bg-[#e58625] font-fractul font-medium text-[14px] text-[#00293D] hover:bg-[#d47920] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
Accept
{isProcessing ? 'Processing...' : 'Accept'}
</button>
</div>
</div>

View File

@@ -1,25 +1,35 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { ConnectionRequestCounts } from '@/services/connection-requests.service';
interface NavItem {
label: string;
href: string;
icon: string;
isActive?: boolean;
interface NetworkSidebarProps {
counts?: ConnectionRequestCounts;
}
const navItems: NavItem[] = [
{
label: 'Connections',
href: '/agent/network/connections',
icon: '/assets/icons/people-icon.svg',
isActive: true,
},
];
interface StatItem {
label: string;
icon: string;
count: number;
color?: string;
}
export function NetworkSidebar({ counts }: NetworkSidebarProps) {
const statItems: StatItem[] = [
{
label: 'Pending Requests',
icon: '/assets/icons/people-icon.svg',
count: counts?.pending || 0,
color: '#E58625',
},
{
label: 'Connections',
icon: '/assets/icons/people-icon.svg',
count: counts?.accepted || 0,
color: '#5ba4a4',
},
];
export function NetworkSidebar() {
return (
<div className="border border-[#00293d] rounded-[7px] p-4 w-full lg:w-[280px] bg-white h-fit">
<h2 className="font-fractul font-medium text-[20px] leading-[24px] text-[#00293D] mb-4">
@@ -28,16 +38,11 @@ export function NetworkSidebar() {
<div className="w-full h-0 border-t border-[#00293d]/30 mb-4" />
<nav className="space-y-3">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 py-2 px-1 rounded transition-colors cursor-pointer ${
item.isActive
? 'text-[#00293D]'
: 'text-[#00293D]/70 hover:text-[#00293D]'
}`}
<div className="space-y-3">
{statItems.map((item) => (
<div
key={item.label}
className="flex items-center gap-3 py-2 px-1"
>
<Image
src={item.icon}
@@ -45,12 +50,30 @@ export function NetworkSidebar() {
width={31}
height={31}
/>
<span className="font-serif font-normal text-[14px] leading-[19px]">
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] flex-1">
{item.label}
</span>
</Link>
<span
className="text-white text-[12px] font-bold rounded-full px-2 py-0.5 min-w-[20px] text-center"
style={{ backgroundColor: item.color }}
>
{item.count}
</span>
</div>
))}
</nav>
</div>
{/* Total Connections Summary */}
<div className="mt-4 pt-4 border-t border-[#00293d]/30">
<div className="flex items-center justify-between">
<span className="font-serif font-normal text-[13px] text-[#00293D]/70">
Total in network
</span>
<span className="font-serif font-bold text-[14px] text-[#00293D]">
{counts?.accepted || 0}
</span>
</div>
</div>
</div>
);
}

View File

@@ -1,80 +1,252 @@
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { NetworkSidebar } from './component/NetworkSidebar';
import { InvitationCard } from './component/InvitationCard';
// Mock data for invitations
const invitations = [
{
id: '1',
name: 'Sathish R',
avatar: '/assets/icons/user-placeholder-icon.svg',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
mutualConnection: 'Anvar',
},
{
id: '2',
name: 'Ragunath',
avatar: '/assets/icons/user-placeholder-icon.svg',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
mutualConnection: 'Anvar',
},
{
id: '3',
name: 'Sabari',
avatar: '/assets/icons/user-placeholder-icon.svg',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
mutualConnection: 'Anvar',
},
];
import { ConnectionCard } from './component/ConnectionCard';
import { connectionRequestsService, ConnectionRequest, ConnectionRequestCounts } from '@/services/connection-requests.service';
export default function NetworkPage() {
const handleAccept = (id: string) => {
console.log('Accepted invitation:', id);
const [pendingRequests, setPendingRequests] = useState<ConnectionRequest[]>([]);
const [connections, setConnections] = useState<ConnectionRequest[]>([]);
const [counts, setCounts] = useState<ConnectionRequestCounts>({ pending: 0, accepted: 0, rejected: 0, total: 0 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
// Fetch connection requests on mount
useEffect(() => {
fetchRequests();
}, []);
const fetchRequests = async () => {
try {
setLoading(true);
setError(null);
// Fetch pending requests, accepted connections, and counts in parallel
const [pending, accepted, requestCounts] = await Promise.all([
connectionRequestsService.getReceivedRequests('PENDING'),
connectionRequestsService.getReceivedRequests('ACCEPTED'),
connectionRequestsService.getRequestCounts(),
]);
setPendingRequests(pending);
setConnections(accepted);
setCounts(requestCounts);
} catch (err) {
console.error('Failed to fetch connection requests:', err);
setError('Failed to load connection requests');
} finally {
setLoading(false);
}
};
const handleIgnore = (id: string) => {
console.log('Ignored invitation:', id);
const handleAccept = async (id: string) => {
if (processingIds.has(id)) return;
try {
setProcessingIds(prev => new Set([...prev, id]));
const updatedRequest = await connectionRequestsService.respondToRequest(id, { status: 'ACCEPTED' });
// Move from pending to connections
const acceptedRequest = pendingRequests.find(r => r.id === id);
if (acceptedRequest) {
setPendingRequests(prev => prev.filter(r => r.id !== id));
setConnections(prev => [{ ...acceptedRequest, ...updatedRequest }, ...prev]);
}
setCounts(prev => ({
...prev,
pending: prev.pending - 1,
accepted: prev.accepted + 1,
}));
} catch (err) {
console.error('Failed to accept request:', err);
} finally {
setProcessingIds(prev => {
const next = new Set(prev);
next.delete(id);
return next;
});
}
};
const handleIgnore = async (id: string) => {
if (processingIds.has(id)) return;
try {
setProcessingIds(prev => new Set([...prev, id]));
await connectionRequestsService.respondToRequest(id, { status: 'REJECTED' });
// Remove from list and update counts
setPendingRequests(prev => prev.filter(r => r.id !== id));
setCounts(prev => ({
...prev,
pending: prev.pending - 1,
rejected: prev.rejected + 1,
}));
} catch (err) {
console.error('Failed to reject request:', err);
} finally {
setProcessingIds(prev => {
const next = new Set(prev);
next.delete(id);
return next;
});
}
};
const handleMessage = (userId: string) => {
// TODO: Navigate to message page or open chat modal
console.log('Message user:', userId);
};
return (
<div className="flex flex-col lg:flex-row gap-6">
{/* Left Sidebar */}
<NetworkSidebar />
<NetworkSidebar counts={counts} />
{/* Main Content */}
<div className="flex-1">
<div className="flex-1 space-y-6">
{/* Header Card */}
<div className="border border-[#00293d]/10 rounded-[15px] px-6 py-4 mb-4 bg-white">
<div className="border border-[#00293d]/10 rounded-[15px] px-6 py-4 bg-white">
<h1 className="font-fractul font-bold text-[15px] leading-[18px] text-[#00293D]">
Manage my network
</h1>
</div>
{/* Invitations Card */}
<div className="border border-[#00293d]/10 rounded-[15px] bg-white">
{/* Invitations Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#00293d]/10">
<h2 className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D]">
Invitations ({invitations.length})
</h2>
<button className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] hover:text-[#e58625] transition-colors cursor-pointer">
Show All
</button>
{/* Loading State */}
{loading && (
<div className="border border-[#00293d]/10 rounded-[15px] bg-white">
<div className="flex items-center justify-center py-12">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#E58625]"></div>
<p className="text-[14px] font-serif text-[#00293D]/70">Loading network...</p>
</div>
</div>
</div>
)}
{/* Invitations List */}
<div className="px-6 divide-y divide-[#00293d]/10">
{invitations.map((invitation) => (
<InvitationCard
key={invitation.id}
{...invitation}
onAccept={handleAccept}
onIgnore={handleIgnore}
/>
))}
{/* Error State */}
{error && !loading && (
<div className="border border-[#00293d]/10 rounded-[15px] bg-white">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<p className="text-[14px] font-serif text-red-500 mb-2">{error}</p>
<button
onClick={fetchRequests}
className="px-4 py-2 bg-[#E58625] rounded-full text-[14px] font-semibold text-white hover:bg-[#E58625]/90 transition-colors"
>
Try Again
</button>
</div>
</div>
</div>
</div>
)}
{!loading && !error && (
<>
{/* Pending Connection Requests Card */}
<div className="border border-[#00293d]/10 rounded-[15px] bg-white">
{/* Requests Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#00293d]/10">
<h2 className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D]">
Pending Requests ({counts.pending})
</h2>
</div>
{/* Empty State for Pending */}
{pendingRequests.length === 0 && (
<div className="flex items-center justify-center py-8">
<div className="text-center">
<div className="w-12 h-12 bg-[#00293D]/5 rounded-full flex items-center justify-center mx-auto mb-3">
<Image
src="/assets/icons/people-icon.svg"
alt="No requests"
width={24}
height={24}
className="opacity-30"
/>
</div>
<h3 className="text-[14px] font-bold font-serif text-[#00293D] mb-1">No pending requests</h3>
<p className="text-[13px] font-serif text-[#00293D]/70">
New connection requests will appear here.
</p>
</div>
</div>
)}
{/* Pending Requests List */}
{pendingRequests.length > 0 && (
<div className="px-6 divide-y divide-[#00293d]/10">
{pendingRequests.map((request) => (
<InvitationCard
key={request.id}
id={request.id}
name={request.user?.email || 'Unknown User'}
avatar={request.user?.avatar || '/assets/icons/user-placeholder-icon.svg'}
description={request.message || 'No message provided'}
createdAt={request.createdAt}
isProcessing={processingIds.has(request.id)}
onAccept={handleAccept}
onIgnore={handleIgnore}
/>
))}
</div>
)}
</div>
{/* My Connections Card */}
<div className="border border-[#00293d]/10 rounded-[15px] bg-white">
{/* Connections Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#00293d]/10">
<h2 className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D]">
My Connections ({counts.accepted})
</h2>
</div>
{/* Empty State for Connections */}
{connections.length === 0 && (
<div className="flex items-center justify-center py-8">
<div className="text-center">
<div className="w-12 h-12 bg-[#00293D]/5 rounded-full flex items-center justify-center mx-auto mb-3">
<Image
src="/assets/icons/people-icon.svg"
alt="No connections"
width={24}
height={24}
className="opacity-30"
/>
</div>
<h3 className="text-[14px] font-bold font-serif text-[#00293D] mb-1">No connections yet</h3>
<p className="text-[13px] font-serif text-[#00293D]/70">
Accept connection requests to grow your network.
</p>
</div>
</div>
)}
{/* Connections List */}
{connections.length > 0 && (
<div className="px-6 divide-y divide-[#00293d]/10">
{connections.map((connection) => (
<ConnectionCard
key={connection.id}
id={connection.userId}
name={connection.user?.email || 'Unknown User'}
avatar={connection.user?.avatar || '/assets/icons/user-placeholder-icon.svg'}
email={connection.user?.email || ''}
connectedAt={connection.respondedAt || connection.updatedAt}
onMessage={handleMessage}
/>
))}
</div>
)}
</div>
</>
)}
</div>
</div>
);

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>
);
}