feat: Implement shimmer loading for profile and avatar images across various components.

This commit is contained in:
pradeepkumar
2026-03-14 14:37:19 +05:30
parent 7b5c670159
commit 9acb0063fa
10 changed files with 83 additions and 101 deletions

View File

@@ -31,17 +31,24 @@ function ProfessionalCard({
profileLink,
}: ProfessionalCardProps) {
const starCount = rating ? Math.round(rating) : 0;
const [imgLoaded, setImgLoaded] = useState(false);
const placeholder = '/assets/icons/user-placeholder-icon.svg';
const isPlaceholder = imageUrl === placeholder;
return (
<Link href={profileLink}>
<div className="bg-white rounded-[15px] overflow-hidden shadow-[0px_4px_20px_rgba(0,0,0,0.08)] hover:shadow-[0px_6px_24px_rgba(0,0,0,0.12)] transition-shadow cursor-pointer">
{/* Image */}
<div className="relative h-[226px] w-full overflow-hidden bg-gray-100">
{!imgLoaded && !isPlaceholder && (
<div className="absolute inset-0 shimmer-loading" />
)}
<Image
src={imageUrl}
alt={name}
fill
className="object-cover"
className={`object-cover transition-opacity duration-300 ${isPlaceholder || imgLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setImgLoaded(true)}
/>
</div>

View File

@@ -203,20 +203,13 @@ export function CommonHeader() {
{/* Avatar */}
<div className="w-[32px] h-[32px] md:w-[35px] md:h-[35px] rounded-full overflow-hidden border-2 border-[#e58625] bg-gray-100 flex-shrink-0 relative">
{(!userImage || !avatarLoaded) && (
<Image
src="/assets/icons/user-placeholder-icon.svg"
alt="Profile"
width={35}
height={35}
className="absolute inset-0 object-cover"
style={{ width: '100%', height: '100%' }}
/>
<div className="absolute inset-0 rounded-full shimmer-loading" />
)}
{userImage && (
<img
src={userImage}
alt="Profile"
className={`w-full h-full object-cover ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
className={`w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setAvatarLoaded(true)}
/>
)}
@@ -240,20 +233,13 @@ export function CommonHeader() {
<div className="flex items-center gap-3 px-4 py-4 border-b border-black/10">
<div className="w-[42px] h-[42px] rounded-full overflow-hidden flex-shrink-0 bg-gray-100 relative">
{(!userImage || !avatarLoaded) && (
<Image
src="/assets/icons/user-placeholder-icon.svg"
alt="Profile"
width={42}
height={42}
className="absolute inset-0 object-cover"
style={{ width: '100%', height: '100%' }}
/>
<div className="absolute inset-0 rounded-full shimmer-loading" />
)}
{userImage && (
<img
src={userImage}
alt="Profile"
className={`w-full h-full object-cover ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
className={`w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setAvatarLoaded(true)}
/>
)}

View File

@@ -124,19 +124,13 @@ export function ChatHeader({
<div className="relative flex-shrink-0">
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20 relative">
{(!avatar || !avatarLoaded || isPlaceholderAvatar) && (
<Image
src={placeholder}
alt={name}
width={50}
height={50}
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 rounded-full shimmer-loading" />
)}
{avatar && !isPlaceholderAvatar && (
<img
src={avatar}
alt={name}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-200 ${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)}
/>
)}

View File

@@ -80,7 +80,7 @@ function formatLastSeen(lastSeenAt: string | null | undefined, isOnline: boolean
return `Last seen ${diffDays}d ago`;
}
// Avatar component with placeholder-until-loaded pattern
// Avatar component with shimmer loading animation
function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; size: number }) {
const [loaded, setLoaded] = useState(false);
const placeholder = '/assets/icons/user-placeholder-icon.svg';
@@ -89,19 +89,13 @@ function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; s
return (
<>
{(!src || !loaded) && (
<Image
src={placeholder}
alt={alt}
width={size}
height={size}
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 rounded-full shimmer-loading" />
)}
{src && !isPlaceholder && (
<img
src={src}
alt={alt}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-200 ${loaded ? 'opacity-100' : 'opacity-0'}`}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setLoaded(true)}
/>
)}
@@ -792,8 +786,8 @@ export function MessagingPage() {
onClick={() => window.open(messageFileUrls[message.id], '_blank')}
/>
) : (
<div className="w-[200px] h-[150px] rounded-[12px] bg-black/5 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#00293d]" />
<div className="w-[200px] h-[150px] rounded-[12px] overflow-hidden">
<div className="w-full h-full shimmer-loading rounded-[12px]" />
</div>
)
) : message.messageType === 'FILE' ? (

View File

@@ -390,19 +390,13 @@ export function ProfileSettingsForm({
<div className="flex items-start gap-4">
<div className="w-[60px] h-[60px] rounded-full overflow-hidden border border-[#00293D]/20 flex-shrink-0 bg-gray-100 relative">
{(!avatarUrl || !avatarLoaded) && (
<Image
src="/assets/icons/user-placeholder-icon.svg"
alt="Profile"
width={60}
height={60}
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 rounded-full shimmer-loading" />
)}
{avatarUrl && (
<img
src={avatarUrl}
alt="Profile"
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-200 ${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)}
/>
)}

View File

@@ -138,19 +138,13 @@ export function SettingsSidebar({
<div className="flex items-center gap-4">
<div className="w-[70px] h-[70px] rounded-full overflow-hidden border-2 border-[#00293D]/20 flex-shrink-0 bg-gray-100 relative">
{(!profileData.avatarUrl || !avatarLoaded) && (
<Image
src="/assets/icons/user-placeholder-icon.svg"
alt={profileData.name}
width={70}
height={70}
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 rounded-full shimmer-loading" />
)}
{profileData.avatarUrl && (
<img
src={profileData.avatarUrl}
alt={profileData.name}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-200 ${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)}
/>
)}