feat: Implement S3 avatar resolution for network connections, refine messaging initial conversation selection, and adjust profile display logic and header navigation.

This commit is contained in:
pradeepkumar
2026-03-19 16:37:24 +05:30
parent 94b7577234
commit 26dc563a49
4 changed files with 52 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { NetworkSidebar } from './component/NetworkSidebar'; import { NetworkSidebar } from './component/NetworkSidebar';
@@ -8,6 +8,7 @@ import { InvitationCard } from './component/InvitationCard';
import { ConnectionCard } from './component/ConnectionCard'; import { ConnectionCard } from './component/ConnectionCard';
import { connectionRequestsService, ConnectionRequest, ConnectionRequestCounts } from '@/services/connection-requests.service'; import { connectionRequestsService, ConnectionRequest, ConnectionRequestCounts } from '@/services/connection-requests.service';
import { messagesService } from '@/services/messages.service'; import { messagesService } from '@/services/messages.service';
import { uploadService } from '@/services/upload.service';
export default function NetworkPage() { export default function NetworkPage() {
const router = useRouter(); const router = useRouter();
@@ -17,6 +18,31 @@ export default function NetworkPage() {
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 [processingIds, setProcessingIds] = useState<Set<string>>(new Set()); const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
const [avatarUrls, setAvatarUrls] = useState<Record<string, string>>({});
// Helper to check if a string is an S3 key (not a URL or local path)
const isS3Key = (value: string | null | undefined): boolean => {
if (!value) return false;
return !value.startsWith('http') && !value.startsWith('/') && !value.startsWith('data:');
};
// Resolve S3 avatar keys to presigned URLs
const resolveAvatars = useCallback(async (requests: ConnectionRequest[]) => {
const newUrls: Record<string, string> = {};
for (const req of requests) {
const avatar = req.user?.userProfile?.avatar || req.user?.avatar;
if (avatar && isS3Key(avatar) && !avatarUrls[req.id]) {
try {
newUrls[req.id] = await uploadService.getPresignedDownloadUrl(avatar);
} catch {
// Keep empty, will show placeholder
}
}
}
if (Object.keys(newUrls).length > 0) {
setAvatarUrls(prev => ({ ...prev, ...newUrls }));
}
}, [avatarUrls]);
// Fetch connection requests on mount // Fetch connection requests on mount
useEffect(() => { useEffect(() => {
@@ -38,6 +64,9 @@ export default function NetworkPage() {
setPendingRequests(pending); setPendingRequests(pending);
setConnections(accepted); setConnections(accepted);
setCounts(requestCounts); setCounts(requestCounts);
// Resolve avatars for all requests
resolveAvatars([...pending, ...accepted]);
} catch (err) { } catch (err) {
console.error('Failed to fetch connection requests:', err); console.error('Failed to fetch connection requests:', err);
setError('Failed to load connection requests'); setError('Failed to load connection requests');
@@ -198,12 +227,14 @@ export default function NetworkPage() {
{pendingRequests.map((request) => { {pendingRequests.map((request) => {
const profile = request.user?.userProfile; const profile = request.user?.userProfile;
const userName = [profile?.firstName, profile?.lastName].filter(Boolean).join(' ') || request.user?.email || 'Unknown User'; const userName = [profile?.firstName, profile?.lastName].filter(Boolean).join(' ') || request.user?.email || 'Unknown User';
const rawAvatar = profile?.avatar || request.user?.avatar;
const resolvedAvatar = avatarUrls[request.id] || (rawAvatar && !isS3Key(rawAvatar) ? rawAvatar : null);
return ( return (
<InvitationCard <InvitationCard
key={request.id} key={request.id}
id={request.id} id={request.id}
name={userName} name={userName}
avatar={profile?.avatar || request.user?.avatar || '/assets/icons/user-placeholder-icon.svg'} avatar={resolvedAvatar || '/assets/icons/user-placeholder-icon.svg'}
description={request.message || 'No message provided'} description={request.message || 'No message provided'}
createdAt={request.createdAt} createdAt={request.createdAt}
isProcessing={processingIds.has(request.id)} isProcessing={processingIds.has(request.id)}
@@ -252,12 +283,14 @@ export default function NetworkPage() {
{connections.map((connection) => { {connections.map((connection) => {
const profile = connection.user?.userProfile; const profile = connection.user?.userProfile;
const userName = [profile?.firstName, profile?.lastName].filter(Boolean).join(' ') || connection.user?.email || 'Unknown User'; const userName = [profile?.firstName, profile?.lastName].filter(Boolean).join(' ') || connection.user?.email || 'Unknown User';
const rawAvatar = profile?.avatar || connection.user?.avatar;
const resolvedAvatar = avatarUrls[connection.id] || (rawAvatar && !isS3Key(rawAvatar) ? rawAvatar : null);
return ( return (
<ConnectionCard <ConnectionCard
key={connection.id} key={connection.id}
id={connection.userId} id={connection.userId}
name={userName} name={userName}
avatar={profile?.avatar || connection.user?.avatar || '/assets/icons/user-placeholder-icon.svg'} avatar={resolvedAvatar || '/assets/icons/user-placeholder-icon.svg'}
email={connection.user?.email || ''} email={connection.user?.email || ''}
connectedAt={connection.respondedAt || connection.updatedAt} connectedAt={connection.respondedAt || connection.updatedAt}
onMessage={handleMessage} onMessage={handleMessage}

View File

@@ -265,12 +265,14 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
const location = getLocation(); const location = getLocation();
const expertiseTags = getExpertiseTags(); const expertiseTags = getExpertiseTags();
const description = getDescription(); const description = getDescription();
const truncatedDescription = description && description.length > 200 const maxDescLength = 150;
? description.slice(0, 200) + '...' const truncatedDescription = description && description.length > maxDescLength
? description.slice(0, maxDescLength) + '...'
: description || ''; : description || '';
const INITIAL_TAG_COUNT = 6;
return ( return (
<div className="bg-white rounded-[20px] p-4 sm:p-5 flex flex-col sm:flex-row gap-4 sm:gap-5 shadow-[0px_10px_20px_rgba(217,217,217,0.5)]"> <div className="bg-white rounded-[20px] p-4 sm:p-5 flex flex-col sm:flex-row gap-4 sm:gap-5 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] min-h-[280px]">
{/* Profile Image & View Profile Button */} {/* Profile Image & View Profile Button */}
<div className="flex-shrink-0 flex flex-col items-center"> <div className="flex-shrink-0 flex flex-col items-center">
<ProfileImage <ProfileImage
@@ -355,7 +357,7 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
<div className="mt-3"> <div className="mt-3">
<p className="font-serif text-[13px] text-[#00293d] leading-[20px]"> <p className="font-serif text-[13px] text-[#00293d] leading-[20px]">
{showFullBio ? description : truncatedDescription} {showFullBio ? description : truncatedDescription}
{description.length > 200 && ( {description.length > maxDescLength && (
<> <>
{' '} {' '}
<button <button
@@ -375,7 +377,7 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
<div className="mt-3"> <div className="mt-3">
<p className="font-fractul font-bold text-[14px] text-[#00293d] mb-2">Expertise:</p> <p className="font-fractul font-bold text-[14px] text-[#00293d] mb-2">Expertise:</p>
<div className="flex flex-wrap gap-2 items-center"> <div className="flex flex-wrap gap-2 items-center">
{(showAllTags ? expertiseTags : expertiseTags.slice(0, 8)).map((tag, index) => ( {(showAllTags ? expertiseTags : expertiseTags.slice(0, INITIAL_TAG_COUNT)).map((tag, index) => (
<span <span
key={index} key={index}
className="border border-[#00293d] rounded-[15px] px-3 py-1 font-serif text-[14px] text-[#00293d]" className="border border-[#00293d] rounded-[15px] px-3 py-1 font-serif text-[14px] text-[#00293d]"
@@ -383,12 +385,12 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
{tag} {tag}
</span> </span>
))} ))}
{expertiseTags.length > 8 && ( {expertiseTags.length > INITIAL_TAG_COUNT && (
<button <button
onClick={() => setShowAllTags(!showAllTags)} onClick={() => setShowAllTags(!showAllTags)}
className="font-serif font-bold text-[14px] text-[#00293d] underline ml-1" className="border border-[#e58625] rounded-[15px] px-3 py-1 font-serif text-[14px] text-[#e58625] hover:bg-[#e58625]/10 transition-colors"
> >
{showAllTags ? 'Show Less' : `+${expertiseTags.length - 8} more`} {showAllTags ? 'Show Less' : `+${expertiseTags.length - INITIAL_TAG_COUNT} more`}
</button> </button>
)} )}
</div> </div>

View File

@@ -57,7 +57,7 @@ export function CommonHeader() {
return ( return (
<header className="bg-[#648188] rounded-[20px] px-4 md:px-8 relative" ref={mobileMenuRef}> <header className="bg-[#648188] rounded-[20px] px-4 md:px-8 relative" ref={mobileMenuRef}>
<div className="flex justify-between items-center h-[60px] md:h-[70px]"> <div className="flex items-center h-[60px] md:h-[70px]">
{/* Logo */} {/* Logo */}
<Link href={dashboardLink} className="flex-shrink-0"> <Link href={dashboardLink} className="flex-shrink-0">
<Image <Image
@@ -70,8 +70,8 @@ export function CommonHeader() {
/> />
</Link> </Link>
{/* Navigation - Desktop only */} {/* Navigation - Desktop only (centered) */}
<nav className="hidden md:flex items-center gap-8 ml-auto mr-8"> <nav className="hidden md:flex items-center gap-8 flex-1 justify-center">
{navLinks.map((link) => ( {navLinks.map((link) => (
<Link <Link
key={link.href} key={link.href}

View File

@@ -302,15 +302,15 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
}, [conversations]); }, [conversations]);
// Auto-select conversation when navigated with initialConversationId // Auto-select conversation when navigated with initialConversationId
const initialConvHandledRef = useRef(false); const initialConvHandledRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if ( if (
initialConversationId && initialConversationId &&
!initialConvHandledRef.current && initialConvHandledRef.current !== initialConversationId &&
conversations.length > 0 && conversations.length > 0 &&
!isLoading !isLoading
) { ) {
initialConvHandledRef.current = true; initialConvHandledRef.current = initialConversationId;
selectConversation(initialConversationId); selectConversation(initialConversationId);
} }
}, [initialConversationId, conversations, isLoading, selectConversation]); }, [initialConversationId, conversations, isLoading, selectConversation]);