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:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { NetworkSidebar } from './component/NetworkSidebar';
|
||||
@@ -8,6 +8,7 @@ import { InvitationCard } from './component/InvitationCard';
|
||||
import { ConnectionCard } from './component/ConnectionCard';
|
||||
import { connectionRequestsService, ConnectionRequest, ConnectionRequestCounts } from '@/services/connection-requests.service';
|
||||
import { messagesService } from '@/services/messages.service';
|
||||
import { uploadService } from '@/services/upload.service';
|
||||
|
||||
export default function NetworkPage() {
|
||||
const router = useRouter();
|
||||
@@ -17,6 +18,31 @@ export default function NetworkPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -38,6 +64,9 @@ export default function NetworkPage() {
|
||||
setPendingRequests(pending);
|
||||
setConnections(accepted);
|
||||
setCounts(requestCounts);
|
||||
|
||||
// Resolve avatars for all requests
|
||||
resolveAvatars([...pending, ...accepted]);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch connection requests:', err);
|
||||
setError('Failed to load connection requests');
|
||||
@@ -198,12 +227,14 @@ export default function NetworkPage() {
|
||||
{pendingRequests.map((request) => {
|
||||
const profile = request.user?.userProfile;
|
||||
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 (
|
||||
<InvitationCard
|
||||
key={request.id}
|
||||
id={request.id}
|
||||
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'}
|
||||
createdAt={request.createdAt}
|
||||
isProcessing={processingIds.has(request.id)}
|
||||
@@ -252,12 +283,14 @@ export default function NetworkPage() {
|
||||
{connections.map((connection) => {
|
||||
const profile = connection.user?.userProfile;
|
||||
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 (
|
||||
<ConnectionCard
|
||||
key={connection.id}
|
||||
id={connection.userId}
|
||||
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 || ''}
|
||||
connectedAt={connection.respondedAt || connection.updatedAt}
|
||||
onMessage={handleMessage}
|
||||
|
||||
@@ -265,12 +265,14 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
||||
const location = getLocation();
|
||||
const expertiseTags = getExpertiseTags();
|
||||
const description = getDescription();
|
||||
const truncatedDescription = description && description.length > 200
|
||||
? description.slice(0, 200) + '...'
|
||||
const maxDescLength = 150;
|
||||
const truncatedDescription = description && description.length > maxDescLength
|
||||
? description.slice(0, maxDescLength) + '...'
|
||||
: description || '';
|
||||
const INITIAL_TAG_COUNT = 6;
|
||||
|
||||
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 */}
|
||||
<div className="flex-shrink-0 flex flex-col items-center">
|
||||
<ProfileImage
|
||||
@@ -355,7 +357,7 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
||||
<div className="mt-3">
|
||||
<p className="font-serif text-[13px] text-[#00293d] leading-[20px]">
|
||||
{showFullBio ? description : truncatedDescription}
|
||||
{description.length > 200 && (
|
||||
{description.length > maxDescLength && (
|
||||
<>
|
||||
{' '}
|
||||
<button
|
||||
@@ -375,7 +377,7 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
||||
<div className="mt-3">
|
||||
<p className="font-fractul font-bold text-[14px] text-[#00293d] mb-2">Expertise:</p>
|
||||
<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
|
||||
key={index}
|
||||
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}
|
||||
</span>
|
||||
))}
|
||||
{expertiseTags.length > 8 && (
|
||||
{expertiseTags.length > INITIAL_TAG_COUNT && (
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function CommonHeader() {
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<Link href={dashboardLink} className="flex-shrink-0">
|
||||
<Image
|
||||
@@ -70,8 +70,8 @@ export function CommonHeader() {
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Navigation - Desktop only */}
|
||||
<nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
|
||||
{/* Navigation - Desktop only (centered) */}
|
||||
<nav className="hidden md:flex items-center gap-8 flex-1 justify-center">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
|
||||
@@ -302,15 +302,15 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
|
||||
}, [conversations]);
|
||||
|
||||
// Auto-select conversation when navigated with initialConversationId
|
||||
const initialConvHandledRef = useRef(false);
|
||||
const initialConvHandledRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (
|
||||
initialConversationId &&
|
||||
!initialConvHandledRef.current &&
|
||||
initialConvHandledRef.current !== initialConversationId &&
|
||||
conversations.length > 0 &&
|
||||
!isLoading
|
||||
) {
|
||||
initialConvHandledRef.current = true;
|
||||
initialConvHandledRef.current = initialConversationId;
|
||||
selectConversation(initialConversationId);
|
||||
}
|
||||
}, [initialConversationId, conversations, isLoading, selectConversation]);
|
||||
|
||||
Reference in New Issue
Block a user