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';
|
'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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user