diff --git a/src/app/(agent)/agent/network/component/ConnectionCard.tsx b/src/app/(agent)/agent/network/component/ConnectionCard.tsx new file mode 100644 index 0000000..99e85eb --- /dev/null +++ b/src/app/(agent)/agent/network/component/ConnectionCard.tsx @@ -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 ( +
+ {/* Avatar */} +
+ {name} +
+ + {/* Content */} +
+ {/* Name */} +

+ {name} +

+ + {/* Email */} +

+ {email} +

+ + {/* Connected date */} + {connectedAt && ( +

+ Connected on {formatDate(connectedAt)} +

+ )} +
+ + {/* Action Buttons */} +
+ +
+
+ ); +} diff --git a/src/app/(agent)/agent/network/component/InvitationCard.tsx b/src/app/(agent)/agent/network/component/InvitationCard.tsx index 3b9ff1a..349eab4 100644 --- a/src/app/(agent)/agent/network/component/InvitationCard.tsx +++ b/src/app/(agent)/agent/network/component/InvitationCard.tsx @@ -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 ( -
+
{/* Avatar */}
- {/* Name with verified icon */} + {/* Name with time */}

{name}

- Verified + {createdAt && ( + + • {formatRelativeTime(createdAt)} + + )}
- {/* Description */} -

+ {/* Description/Message */} +

{description}

- - {/* Mutual Connection */} - {mutualConnection && ( -
- Connection - - Mutual Connection With {mutualConnection} - -
- )}
{/* Action Buttons */}
diff --git a/src/app/(agent)/agent/network/component/NetworkSidebar.tsx b/src/app/(agent)/agent/network/component/NetworkSidebar.tsx index 826a495..2a9267a 100644 --- a/src/app/(agent)/agent/network/component/NetworkSidebar.tsx +++ b/src/app/(agent)/agent/network/component/NetworkSidebar.tsx @@ -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 (

@@ -28,16 +38,11 @@ export function NetworkSidebar() {
- +
+ + {/* Total Connections Summary */} +
+
+ + Total in network + + + {counts?.accepted || 0} + +
+

); } diff --git a/src/app/(agent)/agent/network/page.tsx b/src/app/(agent)/agent/network/page.tsx index 38bac47..3292128 100644 --- a/src/app/(agent)/agent/network/page.tsx +++ b/src/app/(agent)/agent/network/page.tsx @@ -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([]); + const [connections, setConnections] = useState([]); + const [counts, setCounts] = useState({ pending: 0, accepted: 0, rejected: 0, total: 0 }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [processingIds, setProcessingIds] = useState>(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 (
{/* Left Sidebar */} - + {/* Main Content */} -
+
{/* Header Card */} -
+

Manage my network

- {/* Invitations Card */} -
- {/* Invitations Header */} -
-

- Invitations ({invitations.length}) -

- + {/* Loading State */} + {loading && ( +
+
+
+
+

Loading network...

+
+
+ )} - {/* Invitations List */} -
- {invitations.map((invitation) => ( - - ))} + {/* Error State */} + {error && !loading && ( +
+
+
+

{error}

+ +
+
-
+ )} + + {!loading && !error && ( + <> + {/* Pending Connection Requests Card */} +
+ {/* Requests Header */} +
+

+ Pending Requests ({counts.pending}) +

+
+ + {/* Empty State for Pending */} + {pendingRequests.length === 0 && ( +
+
+
+ No requests +
+

No pending requests

+

+ New connection requests will appear here. +

+
+
+ )} + + {/* Pending Requests List */} + {pendingRequests.length > 0 && ( +
+ {pendingRequests.map((request) => ( + + ))} +
+ )} +
+ + {/* My Connections Card */} +
+ {/* Connections Header */} +
+

+ My Connections ({counts.accepted}) +

+
+ + {/* Empty State for Connections */} + {connections.length === 0 && ( +
+
+
+ No connections +
+

No connections yet

+

+ Accept connection requests to grow your network. +

+
+
+ )} + + {/* Connections List */} + {connections.length > 0 && ( +
+ {connections.map((connection) => ( + + ))} +
+ )} +
+ + )}
); diff --git a/src/app/(user)/user/profile/[id]/page.tsx b/src/app/(user)/user/profile/[id]/page.tsx index 2739e76..c3871e5 100644 --- a/src/app/(user)/user/profile/[id]/page.tsx +++ b/src/app/(user)/user/profile/[id]/page.tsx @@ -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(null); const [fieldValues, setFieldValues] = useState([]); const [experienceData, setExperienceData] = useState(defaultExperience); const [specializationFieldsData, setSpecializationFieldsData] = useState(defaultSpecializationFieldsData); + const [profileCardData, setProfileCardData] = useState(defaultProfileCardData); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [imageError, setImageError] = useState(false); + const [avatarUrl, setAvatarUrl] = useState(null); + + // Connect modal state + const [showConnectModal, setShowConnectModal] = useState(false); + const [connectionStatus, setConnectionStatus] = useState(null); + const [connectionRequestId, setConnectionRequestId] = useState(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() {
- {/* Status Buttons - Public view shows availability status */} - + {/* Status Buttons - Public view shows availability status with Connect/Pending/Unlink button */} + setShowConnectModal(true)} + onUnlinkClick={handleUnlink} + /> {/* Contact Info */} 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() {
+ + {/* Connect Request Modal */} + setShowConnectModal(false)} + agentProfileId={id} + agentName={`${agentProfile.firstName || ''} ${agentProfile.lastName || ''}`.trim() || 'Agent'} + existingStatus={connectionStatus} + onSuccess={handleConnectionSuccess} + />
); } diff --git a/src/components/modals/ConnectRequestModal.tsx b/src/components/modals/ConnectRequestModal.tsx new file mode 100644 index 0000000..bd4ecd4 --- /dev/null +++ b/src/components/modals/ConnectRequestModal.tsx @@ -0,0 +1,214 @@ +'use client'; + +import { useState } from 'react'; +import { connectionRequestsService, ConnectionStatus } from '@/services/connection-requests.service'; + +interface ConnectRequestModalProps { + isOpen: boolean; + onClose: () => void; + agentProfileId: string; + agentName: string; + existingStatus?: ConnectionStatus | null; + onSuccess?: () => void; +} + +export function ConnectRequestModal({ + isOpen, + onClose, + agentProfileId, + agentName, + existingStatus, + onSuccess, +}: ConnectRequestModalProps) { + const [message, setMessage] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setError(null); + + try { + await connectionRequestsService.createRequest({ + agentProfileId, + message: message.trim() || undefined, + }); + setSuccess(true); + setMessage(''); + onSuccess?.(); + // Auto close after 2 seconds + setTimeout(() => { + onClose(); + setSuccess(false); + }, 2000); + } catch (err: unknown) { + const errorMessage = + err instanceof Error + ? err.message + : (err as { response?: { data?: { message?: string } } })?.response?.data?.message || + 'Failed to send connection request'; + setError(errorMessage); + } finally { + setIsSubmitting(false); + } + }; + + const handleClose = () => { + setMessage(''); + setError(null); + setSuccess(false); + onClose(); + }; + + // If already connected or pending, show status + if (existingStatus === 'PENDING') { + return ( +
+
+
+
+
+ + + +
+

Request Pending

+

+ Your connection request to {agentName} is pending. Please wait for their response. +

+ +
+
+
+ ); + } + + if (existingStatus === 'ACCEPTED') { + return ( +
+
+
+
+
+ + + +
+

Already Connected

+

+ You are already connected with {agentName}. +

+ +
+
+
+ ); + } + + // Success state + if (success) { + return ( +
+
+
+
+
+ + + +
+

Request Sent!

+

+ Your connection request has been sent to {agentName}. +

+
+
+
+ ); + } + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Close button */} + + + {/* Header */} +
+

Connect with {agentName}

+

+ Send a connection request to start communicating with this agent. +

+
+ + {/* Form */} +
+
+ +