diff --git a/src/app/(agent)/agent/dashboard/page.tsx b/src/app/(agent)/agent/dashboard/page.tsx index f60260a..966ee3d 100644 --- a/src/app/(agent)/agent/dashboard/page.tsx +++ b/src/app/(agent)/agent/dashboard/page.tsx @@ -333,16 +333,17 @@ export default function AgentDashboard() {
{/* Profile Image */}
-
+
+ {/* Initials base layer */} +
+ {agentProfile?.firstName?.[0]?.toUpperCase() || '?'} +
{/* eslint-disable-next-line @next/next/no-img-element */} Profile { - const target = e.target as HTMLImageElement; - target.src = defaultImage; - }} + className="absolute inset-0 w-full h-full object-cover" + onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} /> {/* Gradient Overlay */}
diff --git a/src/app/(agent)/agent/network/component/ConnectionCard.tsx b/src/app/(agent)/agent/network/component/ConnectionCard.tsx index 9e90c82..60653df 100644 --- a/src/app/(agent)/agent/network/component/ConnectionCard.tsx +++ b/src/app/(agent)/agent/network/component/ConnectionCard.tsx @@ -28,6 +28,7 @@ export function ConnectionCard({ onMessage, }: ConnectionCardProps) { const [avatarLoaded, setAvatarLoaded] = useState(false); + const [avatarError, setAvatarError] = useState(false); const placeholder = '/assets/icons/user-placeholder-icon.svg'; const isPlaceholder = !avatar || avatar === placeholder; @@ -36,20 +37,18 @@ export function ConnectionCard({ {/* Avatar */}
- {isPlaceholder ? ( -
- {name} -
- ) : !avatarLoaded ? ( -
- ) : null} - {avatar && !isPlaceholder && ( + {/* Initials base layer */} +
+ {name?.[0]?.toUpperCase() || '?'} +
+ {avatar && !isPlaceholder && !avatarError && ( { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} src={avatar} alt={name} className={`absolute inset-0 w-full h-full object-cover rounded-full transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setAvatarLoaded(true)} + onError={() => setAvatarError(true)} /> )}
diff --git a/src/app/(agent)/agent/network/component/InvitationCard.tsx b/src/app/(agent)/agent/network/component/InvitationCard.tsx index 07b0860..ac9ad5a 100644 --- a/src/app/(agent)/agent/network/component/InvitationCard.tsx +++ b/src/app/(agent)/agent/network/component/InvitationCard.tsx @@ -42,6 +42,7 @@ export function InvitationCard({ onIgnore, }: InvitationCardProps) { const [avatarLoaded, setAvatarLoaded] = useState(false); + const [avatarError, setAvatarError] = useState(false); const placeholder = '/assets/icons/user-placeholder-icon.svg'; const isPlaceholder = !avatar || avatar === placeholder; @@ -50,20 +51,18 @@ export function InvitationCard({ {/* Avatar */}
- {isPlaceholder ? ( -
- {name} -
- ) : !avatarLoaded ? ( -
- ) : null} - {avatar && !isPlaceholder && ( + {/* Initials base layer */} +
+ {name?.[0]?.toUpperCase() || '?'} +
+ {avatar && !isPlaceholder && !avatarError && ( { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} src={avatar} alt={name} className={`absolute inset-0 w-full h-full object-cover rounded-full transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setAvatarLoaded(true)} + onError={() => setAvatarError(true)} /> )}
diff --git a/src/app/(user)/user/profile/[id]/page.tsx b/src/app/(user)/user/profile/[id]/page.tsx index 5ded245..57e5a0c 100644 --- a/src/app/(user)/user/profile/[id]/page.tsx +++ b/src/app/(user)/user/profile/[id]/page.tsx @@ -285,8 +285,12 @@ export default function AgentProfileView() {
{/* Profile Image */}
-
- {getProfileImageUrl() ? ( +
+ {/* Initials base layer */} +
+ {agentProfile?.firstName?.[0]?.toUpperCase() || '?'} +
+ {getProfileImageUrl() && ( <> {!imageLoaded && (
@@ -296,21 +300,11 @@ export default function AgentProfileView() { ref={(el) => { if (el?.complete) { if (el.naturalWidth > 0) setImageLoaded(true); else setImageError(true); } }} src={getProfileImageUrl()!} alt="Profile" - className={`w-full h-full object-cover transition-opacity duration-300 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`} + className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setImageLoaded(true)} onError={() => setImageError(true)} /> - ) : ( -
- Profile -
)} {/* Gradient Overlay */}
diff --git a/src/components/home/TopProfessionals.tsx b/src/components/home/TopProfessionals.tsx index c6a2eef..e9a5da0 100644 --- a/src/components/home/TopProfessionals.tsx +++ b/src/components/home/TopProfessionals.tsx @@ -30,6 +30,7 @@ function ProfessionalCard({ }: ProfessionalCardProps) { const starCount = rating ? Math.round(rating) : 0; const [imgLoaded, setImgLoaded] = useState(false); + const [imgError, setImgError] = useState(false); const placeholder = '/assets/icons/user-placeholder-icon.svg'; const isPlaceholder = imageUrl === placeholder; const INITIAL_TAG_COUNT = 4; @@ -38,17 +39,20 @@ function ProfessionalCard({
{/* Image */}
- {!imgLoaded && !isPlaceholder && ( -
+ {/* Initials base layer */} +
+ {name?.[0]?.toUpperCase() || '?'} +
+ {!isPlaceholder && !imgError && ( + {name} setImgLoaded(true)} + onError={() => setImgError(true)} + /> )} - {name} setImgLoaded(true)} - onError={() => setImgLoaded(true)} - />
{/* Content */} diff --git a/src/components/layout/CommonHeader.tsx b/src/components/layout/CommonHeader.tsx index 147d879..52443f8 100644 --- a/src/components/layout/CommonHeader.tsx +++ b/src/components/layout/CommonHeader.tsx @@ -18,6 +18,7 @@ export function CommonHeader() { const [showProfileMenu, setShowProfileMenu] = useState(false); const [showGuestMenu, setShowGuestMenu] = useState(false); const [showMobileMenu, setShowMobileMenu] = useState(false); + const [avatarError, setAvatarError] = useState(false); const profileMenuRef = useRef(null); const guestMenuRef = useRef(null); const mobileMenuRef = useRef(null); @@ -117,16 +118,18 @@ export function CommonHeader() { > {/* Avatar */}
- {(!userImage || !avatarLoaded) && ( -
- )} - {userImage && ( + {/* Initials base layer */} +
+ {userName?.[0]?.toUpperCase() || '?'} +
+ {userImage && !avatarError && ( { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} src={userImage} alt="Profile" - className={`w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} + className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setAvatarLoaded(true)} + onError={() => setAvatarError(true)} /> )}
@@ -148,16 +151,18 @@ export function CommonHeader() {
- {(!userImage || !avatarLoaded) && ( -
- )} - {userImage && ( + {/* Initials base layer */} +
+ {userName?.[0]?.toUpperCase() || '?'} +
+ {userImage && !avatarError && ( { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} src={userImage} alt="Profile" - className={`w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} + className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setAvatarLoaded(true)} + onError={() => setAvatarError(true)} /> )}
diff --git a/src/components/message/ChatHeader.tsx b/src/components/message/ChatHeader.tsx index 4463a53..d97d99f 100644 --- a/src/components/message/ChatHeader.tsx +++ b/src/components/message/ChatHeader.tsx @@ -43,6 +43,7 @@ export function ChatHeader({ }: ChatHeaderProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); const [avatarLoaded, setAvatarLoaded] = useState(false); + const [avatarError, setAvatarError] = useState(false); const placeholder = '/assets/icons/user-placeholder-icon.svg'; const isPlaceholderAvatar = !avatar || avatar === placeholder; @@ -151,16 +152,18 @@ export function ChatHeader({ {/* Avatar */}
- {(!avatar || !avatarLoaded || isPlaceholderAvatar) && ( -
- )} - {avatar && !isPlaceholderAvatar && ( + {/* Initials base layer */} +
+ {name?.[0]?.toUpperCase() || '?'} +
+ {avatar && !isPlaceholderAvatar && !avatarError && ( { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} src={avatar} alt={name} className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setAvatarLoaded(true)} + onError={() => setAvatarError(true)} /> )}
diff --git a/src/components/message/MessagingPage.tsx b/src/components/message/MessagingPage.tsx index 5377945..d47d304 100644 --- a/src/components/message/MessagingPage.tsx +++ b/src/components/message/MessagingPage.tsx @@ -82,33 +82,28 @@ function formatLastSeen(lastSeenAt: string | null | undefined, isOnline: boolean return `Last seen ${diffDays}d ago`; } -// Avatar component with shimmer loading animation -function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; size: number }) { +// Avatar component with shimmer loading animation and initials fallback +function AvatarWithPlaceholder({ src, alt, size, name }: { src: string; alt: string; size: number; name?: string }) { const [loaded, setLoaded] = useState(false); + const [imgError, setImgError] = useState(false); const placeholder = '/assets/icons/user-placeholder-icon.svg'; const isPlaceholder = !src || src === placeholder; + const initial = name?.[0]?.toUpperCase() || '?'; return ( <> - {(!src || !loaded) && ( -
- )} - {src && !isPlaceholder && ( + {/* Initials base layer - always rendered behind image */} +
+ {initial} +
+ {src && !isPlaceholder && !imgError && ( { if (el?.complete && el.naturalWidth > 0) setLoaded(true); }} src={src} alt={alt} className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setLoaded(true)} - /> - )} - {src && isPlaceholder && ( - {alt} setImgError(true)} /> )} @@ -730,6 +725,7 @@ export function MessagingPage({ initialConversationId }: { initialConversationId src={getValidAvatarUrl(agent.avatarUrl)} alt={agent.name} size={50} + name={agent.name} />
@@ -797,6 +793,7 @@ export function MessagingPage({ initialConversationId }: { initialConversationId src={getValidAvatarUrl(conversationAvatarUrls[conversation.id])} alt={conversation.otherParty?.name || 'User'} size={70} + name={conversation.otherParty?.name} />
{conversation.otherParty?.isOnline && ( diff --git a/src/components/message/NewConversationModal.tsx b/src/components/message/NewConversationModal.tsx index 36252b1..09af9fe 100644 --- a/src/components/message/NewConversationModal.tsx +++ b/src/components/message/NewConversationModal.tsx @@ -130,20 +130,17 @@ export function NewConversationModal({ className="w-full flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors hover:bg-[#00293d]/5 disabled:opacity-50" > {/* Avatar */} -
- {contact.avatarUrl && !contact.avatarUrl.endsWith('user-placeholder-icon.svg') ? ( +
+ {/* Initials base layer */} +
+ {contact.name?.[0]?.toUpperCase() || '?'} +
+ {contact.avatarUrl && !contact.avatarUrl.endsWith('user-placeholder-icon.svg') && ( {contact.name} - ) : ( - {contact.name} { (e.target as HTMLImageElement).style.display = 'none'; }} /> )}