+ {/* 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)}
/>
>
- ) : (
-
-
-
)}
{/* 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 && (
+
setImgLoaded(true)}
+ onError={() => setImgError(true)}
+ />
)}
- 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 && (
-
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') && (
})
- ) : (
-
{ (e.target as HTMLImageElement).style.display = 'none'; }}
/>
)}