feat: Improve avatar loading experience by displaying placeholders until images are fully loaded across various components.

This commit is contained in:
pradeepkumar
2026-03-13 22:45:11 +05:30
parent b339dd865c
commit 7b5c670159
11 changed files with 225 additions and 80 deletions

View File

@@ -46,12 +46,18 @@ export function ProfileSettingsForm({
});
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [avatarLoaded, setAvatarLoaded] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Reset avatar loaded state when URL changes (e.g., after upload)
useEffect(() => {
setAvatarLoaded(false);
}, [avatarUrl]);
// Store original data for cancel/reset functionality
const [originalData, setOriginalData] = useState<typeof formData | null>(null);
// Track if initial profile fetch is done to avoid re-showing loading spinner
@@ -382,20 +388,22 @@ export function ProfileSettingsForm({
{/* Profile Photo Section */}
<div className="mb-8">
<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">
{avatarUrl ? (
<img
src={avatarUrl}
alt="Profile"
className="w-full h-full object-cover"
/>
) : (
<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="w-full h-full object-cover"
className="absolute inset-0 w-full h-full object-cover"
/>
)}
{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'}`}
onLoad={() => setAvatarLoaded(true)}
/>
)}
</div>

View File

@@ -25,6 +25,8 @@ export function SettingsSidebar({
const pathname = usePathname();
const { data: session } = useSession();
const [avatarLoaded, setAvatarLoaded] = useState(false);
const [profileData, setProfileData] = useState({
name: session?.user?.name || 'User',
title: 'Real Estate Agent',
@@ -65,6 +67,7 @@ export function SettingsSidebar({
}
setProfileData({ name, title, avatarUrl });
setAvatarLoaded(false);
} catch (err) {
console.error('Failed to fetch profile for sidebar:', err);
}
@@ -133,20 +136,22 @@ export function SettingsSidebar({
{/* Profile Card */}
<div className="bg-white rounded-[15px] border border-[#00293D]/20 p-5">
<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">
{profileData.avatarUrl ? (
<img
src={profileData.avatarUrl}
alt={profileData.name}
className="w-full h-full object-cover"
/>
) : (
<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="w-full h-full object-cover"
className="absolute inset-0 w-full h-full object-cover"
/>
)}
{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'}`}
onLoad={() => setAvatarLoaded(true)}
/>
)}
</div>