feat: Implement agent-user connection request system with a new service and dynamic profile card buttons.
This commit is contained in:
79
src/app/(agent)/agent/network/component/ConnectionCard.tsx
Normal file
79
src/app/(agent)/agent/network/component/ConnectionCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,22 +7,41 @@ interface InvitationCardProps {
|
|||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
description: string;
|
description: string;
|
||||||
mutualConnection?: string;
|
createdAt?: string;
|
||||||
|
isProcessing?: boolean;
|
||||||
onAccept?: (id: string) => void;
|
onAccept?: (id: string) => void;
|
||||||
onIgnore?: (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({
|
export function InvitationCard({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
avatar,
|
avatar,
|
||||||
description,
|
description,
|
||||||
mutualConnection,
|
createdAt,
|
||||||
|
isProcessing = false,
|
||||||
onAccept,
|
onAccept,
|
||||||
onIgnore,
|
onIgnore,
|
||||||
}: InvitationCardProps) {
|
}: InvitationCardProps) {
|
||||||
return (
|
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 */}
|
{/* Avatar */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
@@ -36,53 +55,39 @@ export function InvitationCard({
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{/* Name with verified icon */}
|
{/* Name with time */}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<h3 className="font-serif font-bold text-[14px] leading-[19px] text-[#00293D]">
|
<h3 className="font-serif font-bold text-[14px] leading-[19px] text-[#00293D]">
|
||||||
{name}
|
{name}
|
||||||
</h3>
|
</h3>
|
||||||
<Image
|
{createdAt && (
|
||||||
src="/assets/icons/shield-verified-icon.svg"
|
<span className="font-serif font-normal text-[12px] text-[#00293D]/50">
|
||||||
alt="Verified"
|
• {formatRelativeTime(createdAt)}
|
||||||
width={19}
|
</span>
|
||||||
height={19}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description/Message */}
|
||||||
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] mb-1">
|
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] line-clamp-2">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex items-center gap-3 flex-shrink-0">
|
<div className="flex items-center gap-3 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => onIgnore?.(id)}
|
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
|
Ignore
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onAccept?.(id)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import { ConnectionRequestCounts } from '@/services/connection-requests.service';
|
||||||
|
|
||||||
interface NavItem {
|
interface NetworkSidebarProps {
|
||||||
label: string;
|
counts?: ConnectionRequestCounts;
|
||||||
href: string;
|
|
||||||
icon: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
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',
|
label: 'Connections',
|
||||||
href: '/agent/network/connections',
|
|
||||||
icon: '/assets/icons/people-icon.svg',
|
icon: '/assets/icons/people-icon.svg',
|
||||||
isActive: true,
|
count: counts?.accepted || 0,
|
||||||
|
color: '#5ba4a4',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function NetworkSidebar() {
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-[#00293d] rounded-[7px] p-4 w-full lg:w-[280px] bg-white h-fit">
|
<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">
|
<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" />
|
<div className="w-full h-0 border-t border-[#00293d]/30 mb-4" />
|
||||||
|
|
||||||
<nav className="space-y-3">
|
<div className="space-y-3">
|
||||||
{navItems.map((item) => (
|
{statItems.map((item) => (
|
||||||
<Link
|
<div
|
||||||
key={item.href}
|
key={item.label}
|
||||||
href={item.href}
|
className="flex items-center gap-3 py-2 px-1"
|
||||||
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]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={item.icon}
|
src={item.icon}
|
||||||
@@ -45,12 +50,30 @@ export function NetworkSidebar() {
|
|||||||
width={31}
|
width={31}
|
||||||
height={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}
|
{item.label}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,252 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
import { NetworkSidebar } from './component/NetworkSidebar';
|
import { NetworkSidebar } from './component/NetworkSidebar';
|
||||||
import { InvitationCard } from './component/InvitationCard';
|
import { InvitationCard } from './component/InvitationCard';
|
||||||
|
import { ConnectionCard } from './component/ConnectionCard';
|
||||||
// Mock data for invitations
|
import { connectionRequestsService, ConnectionRequest, ConnectionRequestCounts } from '@/services/connection-requests.service';
|
||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function NetworkPage() {
|
export default function NetworkPage() {
|
||||||
const handleAccept = (id: string) => {
|
const [pendingRequests, setPendingRequests] = useState<ConnectionRequest[]>([]);
|
||||||
console.log('Accepted invitation:', id);
|
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) => {
|
const handleAccept = async (id: string) => {
|
||||||
console.log('Ignored invitation:', id);
|
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 (
|
return (
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
{/* Left Sidebar */}
|
{/* Left Sidebar */}
|
||||||
<NetworkSidebar />
|
<NetworkSidebar counts={counts} />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1">
|
<div className="flex-1 space-y-6">
|
||||||
{/* Header Card */}
|
{/* 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]">
|
<h1 className="font-fractul font-bold text-[15px] leading-[18px] text-[#00293D]">
|
||||||
Manage my network
|
Manage my network
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Invitations Card */}
|
{/* Loading State */}
|
||||||
|
{loading && (
|
||||||
<div className="border border-[#00293d]/10 rounded-[15px] bg-white">
|
<div className="border border-[#00293d]/10 rounded-[15px] bg-white">
|
||||||
{/* Invitations Header */}
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[#00293d]/10">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<h2 className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D]">
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#E58625]"></div>
|
||||||
Invitations ({invitations.length})
|
<p className="text-[14px] font-serif text-[#00293D]/70">Loading network...</p>
|
||||||
</h2>
|
</div>
|
||||||
<button className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] hover:text-[#e58625] transition-colors cursor-pointer">
|
</div>
|
||||||
Show All
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Invitations List */}
|
{!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">
|
<div className="px-6 divide-y divide-[#00293d]/10">
|
||||||
{invitations.map((invitation) => (
|
{pendingRequests.map((request) => (
|
||||||
<InvitationCard
|
<InvitationCard
|
||||||
key={invitation.id}
|
key={request.id}
|
||||||
{...invitation}
|
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}
|
onAccept={handleAccept}
|
||||||
onIgnore={handleIgnore}
|
onIgnore={handleIgnore}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
// Import shared components
|
// Import shared components
|
||||||
import {
|
import {
|
||||||
@@ -14,9 +15,11 @@ import {
|
|||||||
StatusButtons,
|
StatusButtons,
|
||||||
ContactInfo,
|
ContactInfo,
|
||||||
} from '@/components/profile';
|
} from '@/components/profile';
|
||||||
|
import { ConnectRequestModal } from '@/components/modals';
|
||||||
import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agents.service';
|
import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agents.service';
|
||||||
import { getProxyImageUrl } from '@/lib/imageProxy';
|
import { connectionRequestsService, ConnectionStatus } from '@/services/connection-requests.service';
|
||||||
import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, ExperienceData, SpecializationFieldsData } from '@/utils/profileDataMapper';
|
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
|
// Mock data for sections not yet available from API
|
||||||
const mockData = {
|
const mockData = {
|
||||||
@@ -65,20 +68,44 @@ const defaultSpecializationFieldsData: SpecializationFieldsData = {
|
|||||||
fields: [],
|
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() {
|
export default function AgentProfileView() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id as string;
|
const id = params.id as string;
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null);
|
const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null);
|
||||||
const [fieldValues, setFieldValues] = useState<FieldValueResponse[]>([]);
|
const [fieldValues, setFieldValues] = useState<FieldValueResponse[]>([]);
|
||||||
const [experienceData, setExperienceData] = useState<ExperienceData>(defaultExperience);
|
const [experienceData, setExperienceData] = useState<ExperienceData>(defaultExperience);
|
||||||
const [specializationFieldsData, setSpecializationFieldsData] = useState<SpecializationFieldsData>(defaultSpecializationFieldsData);
|
const [specializationFieldsData, setSpecializationFieldsData] = useState<SpecializationFieldsData>(defaultSpecializationFieldsData);
|
||||||
|
const [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [imageError, setImageError] = useState(false);
|
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';
|
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
|
// Fetch agent profile and field values on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -104,6 +131,24 @@ export default function AgentProfileView() {
|
|||||||
// Map field values to specialization fields data (only fields from "Specialization" section)
|
// Map field values to specialization fields data (only fields from "Specialization" section)
|
||||||
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues);
|
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues);
|
||||||
setSpecializationFieldsData(mappedSpecializationFields);
|
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) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch profile:', err);
|
console.error('Failed to fetch profile:', err);
|
||||||
setError('Failed to load profile data');
|
setError('Failed to load profile data');
|
||||||
@@ -115,24 +160,65 @@ export default function AgentProfileView() {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [id]);
|
}, [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 = () => {
|
const getProfileImageUrl = () => {
|
||||||
// If image failed to load, return default
|
// If image failed to load, return default
|
||||||
if (imageError) {
|
if (imageError) {
|
||||||
return defaultImage;
|
return defaultImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have a presigned URL from S3, use it
|
||||||
|
if (avatarUrl) {
|
||||||
|
return avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for null, undefined, or empty string
|
// Check for null, undefined, or empty string
|
||||||
if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') {
|
if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') {
|
||||||
return defaultImage;
|
return defaultImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// S3 URLs need to go through the backend proxy
|
// For relative paths (local assets), return as-is
|
||||||
if (agentProfile.avatar.startsWith('http')) {
|
if (agentProfile.avatar.startsWith('/')) {
|
||||||
return getProxyImageUrl(agentProfile.avatar);
|
return agentProfile.avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For relative paths (local assets), return as-is
|
// Default fallback
|
||||||
return agentProfile.avatar;
|
return defaultImage;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format member since date
|
// Format member since date
|
||||||
@@ -208,8 +294,13 @@ export default function AgentProfileView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Buttons - Public view shows availability status */}
|
{/* Status Buttons - Public view shows availability status with Connect/Pending/Unlink button */}
|
||||||
<StatusButtons isAvailable={agentProfile.isAvailable ?? true} />
|
<StatusButtons
|
||||||
|
isAvailable={agentProfile.isAvailable ?? true}
|
||||||
|
connectionStatus={connectionStatus}
|
||||||
|
onConnectClick={() => setShowConnectModal(true)}
|
||||||
|
onUnlinkClick={handleUnlink}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Contact Info */}
|
{/* Contact Info */}
|
||||||
<ContactInfo
|
<ContactInfo
|
||||||
@@ -226,12 +317,17 @@ export default function AgentProfileView() {
|
|||||||
lastName={agentProfile.lastName}
|
lastName={agentProfile.lastName}
|
||||||
isVerified={agentProfile.isVerified}
|
isVerified={agentProfile.isVerified}
|
||||||
title={agentProfile.agentType?.name || 'Real Estate Agent'}
|
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)}
|
memberSince={formatMemberSince((agentProfile as unknown as { createdAt?: string }).createdAt)}
|
||||||
bio={agentProfile.bio || ''}
|
bio={profileCardData.bio || agentProfile.bio || ''}
|
||||||
expertise={agentProfile.specializations || []}
|
expertise={profileCardData.expertise.length > 0 ? profileCardData.expertise : (agentProfile.specializations || [])}
|
||||||
showEditButton={false}
|
showEditButton={false}
|
||||||
messageHref="/user/message"
|
messageHref="/user/message"
|
||||||
|
connectionStatus={connectionStatus}
|
||||||
|
onConnectClick={() => setShowConnectModal(true)}
|
||||||
|
onUnlinkClick={handleUnlink}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Experience Section - Dynamic data from profile fields */}
|
{/* Experience Section - Dynamic data from profile fields */}
|
||||||
@@ -293,6 +389,16 @@ export default function AgentProfileView() {
|
|||||||
<TestimonialsSection testimonials={mockData.testimonials} />
|
<TestimonialsSection testimonials={mockData.testimonials} />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
214
src/components/modals/ConnectRequestModal.tsx
Normal file
214
src/components/modals/ConnectRequestModal.tsx
Normal file
@@ -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<string | null>(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 (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div className="absolute inset-0 bg-black/50" onClick={handleClose} />
|
||||||
|
<div className="relative bg-white rounded-[20px] p-6 w-full max-w-md mx-4 shadow-xl">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-[#00293D] mb-2">Request Pending</h3>
|
||||||
|
<p className="text-sm text-[#00293D]/70 mb-4">
|
||||||
|
Your connection request to {agentName} is pending. Please wait for their response.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="px-6 py-2 bg-[#00293D] rounded-full text-sm font-semibold text-white hover:bg-[#00293D]/90 transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingStatus === 'ACCEPTED') {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div className="absolute inset-0 bg-black/50" onClick={handleClose} />
|
||||||
|
<div className="relative bg-white rounded-[20px] p-6 w-full max-w-md mx-4 shadow-xl">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-[#00293D] mb-2">Already Connected</h3>
|
||||||
|
<p className="text-sm text-[#00293D]/70 mb-4">
|
||||||
|
You are already connected with {agentName}.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="px-6 py-2 bg-[#00293D] rounded-full text-sm font-semibold text-white hover:bg-[#00293D]/90 transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success state
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div className="absolute inset-0 bg-black/50" onClick={handleClose} />
|
||||||
|
<div className="relative bg-white rounded-[20px] p-6 w-full max-w-md mx-4 shadow-xl">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-[#00293D] mb-2">Request Sent!</h3>
|
||||||
|
<p className="text-sm text-[#00293D]/70">
|
||||||
|
Your connection request has been sent to {agentName}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-black/50" onClick={handleClose} />
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative bg-white rounded-[20px] p-6 w-full max-w-md mx-4 shadow-xl">
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="absolute top-4 right-4 text-[#00293D]/50 hover:text-[#00293D] transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-xl font-bold text-[#00293D]">Connect with {agentName}</h2>
|
||||||
|
<p className="text-sm text-[#00293D]/70 mt-1">
|
||||||
|
Send a connection request to start communicating with this agent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-[#00293D] mb-2">
|
||||||
|
Message (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="Introduce yourself and explain what you're looking for..."
|
||||||
|
rows={4}
|
||||||
|
maxLength={1000}
|
||||||
|
className="w-full px-4 py-3 border border-[#00293D]/20 rounded-[10px] text-sm text-[#00293D] placeholder:text-[#00293D]/40 focus:outline-none focus:border-[#E58625] resize-none"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-[#00293D]/50 mt-1 text-right">
|
||||||
|
{message.length}/1000
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-[10px] text-sm text-red-600">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="flex-1 px-4 py-3 border border-[#00293D]/20 rounded-full text-sm font-semibold text-[#00293D] hover:bg-[#00293D]/5 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="flex-1 px-4 py-3 bg-[#E58625] rounded-full text-sm font-semibold text-white hover:bg-[#E58625]/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Sending...' : 'Send Request'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/modals/index.ts
Normal file
1
src/components/modals/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './ConnectRequestModal';
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { Tag } from './Tag';
|
import { Tag } from './Tag';
|
||||||
|
|
||||||
|
type ConnectionStatus = 'PENDING' | 'ACCEPTED' | 'REJECTED' | null;
|
||||||
|
|
||||||
interface ProfileCardProps {
|
interface ProfileCardProps {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
@@ -19,6 +22,9 @@ interface ProfileCardProps {
|
|||||||
editHref?: string;
|
editHref?: string;
|
||||||
messageHref?: string;
|
messageHref?: string;
|
||||||
requestsHref?: string;
|
requestsHref?: string;
|
||||||
|
connectionStatus?: ConnectionStatus;
|
||||||
|
onConnectClick?: () => void; // Callback when Connect button is clicked (for opening modal)
|
||||||
|
onUnlinkClick?: () => void; // Callback when Unlink button is clicked
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileCard({
|
export function ProfileCard({
|
||||||
@@ -34,10 +40,25 @@ export function ProfileCard({
|
|||||||
editHref = '/agent/edit',
|
editHref = '/agent/edit',
|
||||||
messageHref,
|
messageHref,
|
||||||
requestsHref,
|
requestsHref,
|
||||||
|
connectionStatus,
|
||||||
|
onConnectClick,
|
||||||
|
onUnlinkClick,
|
||||||
}: ProfileCardProps) {
|
}: ProfileCardProps) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const [isUnlinking, setIsUnlinking] = useState(false);
|
||||||
|
|
||||||
|
// Handle unlink click
|
||||||
|
const handleUnlinkClick = async () => {
|
||||||
|
if (isUnlinking) return;
|
||||||
|
setIsUnlinking(true);
|
||||||
|
try {
|
||||||
|
await onUnlinkClick?.();
|
||||||
|
} finally {
|
||||||
|
setIsUnlinking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle action click - require login if not authenticated
|
// Handle action click - require login if not authenticated
|
||||||
const handleActionClick = (e: React.MouseEvent, targetHref?: string) => {
|
const handleActionClick = (e: React.MouseEvent, targetHref?: string) => {
|
||||||
@@ -146,7 +167,8 @@ export function ProfileCard({
|
|||||||
Message
|
Message
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{requestsHref ? (
|
{showEditButton && requestsHref ? (
|
||||||
|
// Agent's own profile - show Requests link
|
||||||
<Link
|
<Link
|
||||||
href={requestsHref}
|
href={requestsHref}
|
||||||
onClick={(e) => handleActionClick(e, requestsHref)}
|
onClick={(e) => handleActionClick(e, requestsHref)}
|
||||||
@@ -154,15 +176,52 @@ export function ProfileCard({
|
|||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/assets/icons/requests-dark-icon.svg"
|
src="/assets/icons/requests-dark-icon.svg"
|
||||||
alt="Connect"
|
alt="Requests"
|
||||||
width={14}
|
width={14}
|
||||||
height={13}
|
height={13}
|
||||||
/>
|
/>
|
||||||
{showEditButton ? 'Requests' : 'Connect'}
|
Requests
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
// Public view - show Connect/Pending/Unlink button based on connection status
|
||||||
|
connectionStatus === 'PENDING' ? (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleActionClick(e)}
|
disabled
|
||||||
|
className="flex items-center gap-2 px-4 py-1.5 bg-[#fef3c7] text-[#92600f] text-sm font-normal rounded-[15px] border border-[#00293d]/10 cursor-not-allowed font-serif"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/requests-dark-icon.svg"
|
||||||
|
alt="Pending"
|
||||||
|
width={14}
|
||||||
|
height={13}
|
||||||
|
/>
|
||||||
|
Pending
|
||||||
|
</button>
|
||||||
|
) : connectionStatus === 'ACCEPTED' ? (
|
||||||
|
<button
|
||||||
|
onClick={handleUnlinkClick}
|
||||||
|
disabled={isUnlinking}
|
||||||
|
className="flex items-center gap-2 px-4 py-1.5 bg-red-500 text-white text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-red-600 transition-colors font-serif cursor-pointer disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/requests-dark-icon.svg"
|
||||||
|
alt="Unlink"
|
||||||
|
width={14}
|
||||||
|
height={13}
|
||||||
|
/>
|
||||||
|
{isUnlinking ? 'Unlinking...' : 'Unlink'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!session) {
|
||||||
|
e.preventDefault();
|
||||||
|
const returnUrl = encodeURIComponent(pathname);
|
||||||
|
router.push(`/login?callbackUrl=${returnUrl}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onConnectClick?.();
|
||||||
|
}}
|
||||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -171,8 +230,9 @@ export function ProfileCard({
|
|||||||
width={14}
|
width={14}
|
||||||
height={13}
|
height={13}
|
||||||
/>
|
/>
|
||||||
{showEditButton ? 'Requests' : 'Connect'}
|
Connect
|
||||||
</button>
|
</button>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
type ConnectionStatus = 'PENDING' | 'ACCEPTED' | 'REJECTED' | null;
|
||||||
|
|
||||||
interface StatusButtonsProps {
|
interface StatusButtonsProps {
|
||||||
isAvailable?: boolean;
|
isAvailable?: boolean;
|
||||||
|
connectionStatus?: ConnectionStatus;
|
||||||
onToggleAvailability?: () => void;
|
onToggleAvailability?: () => void;
|
||||||
|
onConnectClick?: () => void; // Callback when Connect button is clicked (for opening modal)
|
||||||
|
onUnlinkClick?: () => void; // Callback when Unlink button is clicked
|
||||||
showToggle?: boolean; // If true, show as toggle (for agent's own dashboard)
|
showToggle?: boolean; // If true, show as toggle (for agent's own dashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusButtons({
|
export function StatusButtons({
|
||||||
isAvailable = true,
|
isAvailable = true,
|
||||||
|
connectionStatus = null,
|
||||||
onToggleAvailability,
|
onToggleAvailability,
|
||||||
|
onConnectClick,
|
||||||
|
onUnlinkClick,
|
||||||
showToggle = false
|
showToggle = false
|
||||||
}: StatusButtonsProps) {
|
}: StatusButtonsProps) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const [isUnlinking, setIsUnlinking] = useState(false);
|
||||||
|
|
||||||
// Handle connect click - require login if not authenticated
|
// Handle connect click - require login if not authenticated
|
||||||
const handleConnectClick = () => {
|
const handleConnectClick = () => {
|
||||||
@@ -28,7 +38,63 @@ export function StatusButtons({
|
|||||||
router.push(`/login?callbackUrl=${returnUrl}`);
|
router.push(`/login?callbackUrl=${returnUrl}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: Handle connect action when logged in
|
|
||||||
|
// Call the onConnectClick callback if provided
|
||||||
|
onConnectClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle unlink click
|
||||||
|
const handleUnlinkClick = async () => {
|
||||||
|
if (isUnlinking) return;
|
||||||
|
setIsUnlinking(true);
|
||||||
|
try {
|
||||||
|
await onUnlinkClick?.();
|
||||||
|
} finally {
|
||||||
|
setIsUnlinking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render the appropriate button based on connection status
|
||||||
|
const renderConnectionButton = () => {
|
||||||
|
// If PENDING - show yellow Pending button (disabled)
|
||||||
|
if (connectionStatus === 'PENDING') {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className="px-5 py-1.5 text-[#92600f] text-sm rounded-[15px] bg-[#fef3c7] cursor-not-allowed font-fractul"
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ACCEPTED - show Unlink button
|
||||||
|
if (connectionStatus === 'ACCEPTED') {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleUnlinkClick}
|
||||||
|
disabled={isUnlinking}
|
||||||
|
className="px-5 py-1.5 text-white text-sm rounded-[15px] bg-red-500 hover:bg-red-600 transition-colors font-fractul disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isUnlinking ? 'Unlinking...' : 'Unlink'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: No request or REJECTED - show Connect button
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleConnectClick}
|
||||||
|
disabled={!isAvailable}
|
||||||
|
className={`px-5 py-1.5 text-white text-sm rounded-[15px] transition-colors font-fractul ${
|
||||||
|
isAvailable
|
||||||
|
? 'bg-[#e58625] hover:bg-[#d47720] cursor-pointer'
|
||||||
|
: 'bg-[#00293d] cursor-not-allowed opacity-70'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// If showToggle is true, render both options as a toggle selector (for agent dashboard)
|
// If showToggle is true, render both options as a toggle selector (for agent dashboard)
|
||||||
@@ -78,7 +144,7 @@ export function StatusButtons({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public view - show single status with Connect button
|
// Public view - show single status with Connect/Pending/Unlink button
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-[354px] lg:max-w-none">
|
<div className="w-full max-w-[354px] lg:max-w-none">
|
||||||
<div className="flex items-center border border-[#00293d]/10 rounded-[15px] h-[50px] px-4">
|
<div className="flex items-center border border-[#00293d]/10 rounded-[15px] h-[50px] px-4">
|
||||||
@@ -90,17 +156,7 @@ export function StatusButtons({
|
|||||||
{isAvailable ? 'Available.' : 'Unavailable.'}
|
{isAvailable ? 'Available.' : 'Unavailable.'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
{renderConnectionButton()}
|
||||||
onClick={handleConnectClick}
|
|
||||||
disabled={!isAvailable}
|
|
||||||
className={`px-5 py-1.5 text-white text-sm rounded-[15px] transition-colors font-fractul ${
|
|
||||||
isAvailable
|
|
||||||
? 'bg-[#e58625] hover:bg-[#d47720] cursor-pointer'
|
|
||||||
: 'bg-[#00293d] cursor-not-allowed opacity-70'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
151
src/services/connection-requests.service.ts
Normal file
151
src/services/connection-requests.service.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import api from './api';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type ConnectionStatus = 'PENDING' | 'ACCEPTED' | 'REJECTED';
|
||||||
|
|
||||||
|
export interface ConnectionRequest {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
agentProfileId: string;
|
||||||
|
status: ConnectionStatus;
|
||||||
|
message: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
respondedAt: string | null;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
avatar: string | null;
|
||||||
|
};
|
||||||
|
agentProfile?: {
|
||||||
|
id: string;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
avatar: string | null;
|
||||||
|
headline: string | null;
|
||||||
|
city: string | null;
|
||||||
|
state: string | null;
|
||||||
|
isVerified: boolean;
|
||||||
|
agentType?: {
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateConnectionRequestDto {
|
||||||
|
agentProfileId: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespondConnectionRequestDto {
|
||||||
|
status: 'ACCEPTED' | 'REJECTED';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionRequestCounts {
|
||||||
|
pending: number;
|
||||||
|
accepted: number;
|
||||||
|
rejected: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionStatusResponse {
|
||||||
|
id: string;
|
||||||
|
status: ConnectionStatus;
|
||||||
|
createdAt: string;
|
||||||
|
respondedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data: T;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionRequestsService {
|
||||||
|
private basePath = '/connection-requests';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new connection request to an agent
|
||||||
|
*/
|
||||||
|
async createRequest(dto: CreateConnectionRequestDto): Promise<ConnectionRequest> {
|
||||||
|
const response = await api.post<ApiResponse<ConnectionRequest>>(this.basePath, dto);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all connection requests sent by the current user
|
||||||
|
*/
|
||||||
|
async getMyRequests(status?: ConnectionStatus): Promise<ConnectionRequest[]> {
|
||||||
|
const params = status ? { status } : {};
|
||||||
|
const response = await api.get<ApiResponse<ConnectionRequest[]>>(
|
||||||
|
`${this.basePath}/my-requests`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all connection requests received by the current agent
|
||||||
|
*/
|
||||||
|
async getReceivedRequests(status?: ConnectionStatus): Promise<ConnectionRequest[]> {
|
||||||
|
const params = status ? { status } : {};
|
||||||
|
const response = await api.get<ApiResponse<ConnectionRequest[]>>(
|
||||||
|
`${this.basePath}/received`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection request counts for the current agent
|
||||||
|
*/
|
||||||
|
async getRequestCounts(): Promise<ConnectionRequestCounts> {
|
||||||
|
const response = await api.get<ApiResponse<ConnectionRequestCounts>>(
|
||||||
|
`${this.basePath}/received/counts`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection status between current user and an agent
|
||||||
|
*/
|
||||||
|
async getConnectionStatus(agentProfileId: string): Promise<ConnectionStatusResponse | null> {
|
||||||
|
const response = await api.get<ApiResponse<ConnectionStatusResponse | null>>(
|
||||||
|
`${this.basePath}/status/${agentProfileId}`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific connection request by ID
|
||||||
|
*/
|
||||||
|
async getRequestById(requestId: string): Promise<ConnectionRequest> {
|
||||||
|
const response = await api.get<ApiResponse<ConnectionRequest>>(
|
||||||
|
`${this.basePath}/${requestId}`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respond to a connection request (accept/reject)
|
||||||
|
*/
|
||||||
|
async respondToRequest(
|
||||||
|
requestId: string,
|
||||||
|
dto: RespondConnectionRequestDto
|
||||||
|
): Promise<ConnectionRequest> {
|
||||||
|
const response = await api.patch<ApiResponse<ConnectionRequest>>(
|
||||||
|
`${this.basePath}/${requestId}/respond`,
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a pending connection request
|
||||||
|
*/
|
||||||
|
async cancelRequest(requestId: string): Promise<void> {
|
||||||
|
await api.delete(`${this.basePath}/${requestId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const connectionRequestsService = new ConnectionRequestsService();
|
||||||
Reference in New Issue
Block a user