feat: Implement shimmer loading for profile and avatar images across various components.
This commit is contained in:
@@ -37,19 +37,13 @@ export function ConnectionCard({
|
|||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="w-[80px] h-[80px] rounded-full overflow-hidden relative">
|
<div className="w-[80px] h-[80px] rounded-full overflow-hidden relative">
|
||||||
{(!avatar || !avatarLoaded || isPlaceholder) && (
|
{(!avatar || !avatarLoaded || isPlaceholder) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src={placeholder}
|
|
||||||
alt={name}
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{avatar && !isPlaceholder && (
|
{avatar && !isPlaceholder && (
|
||||||
<img
|
<img
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt={name}
|
alt={name}
|
||||||
className={`absolute inset-0 w-full h-full object-cover rounded-full transition-opacity duration-200 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
|
className={`absolute inset-0 w-full h-full object-cover rounded-full transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||||
onLoad={() => setAvatarLoaded(true)}
|
onLoad={() => setAvatarLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -51,19 +51,13 @@ export function InvitationCard({
|
|||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="w-[80px] h-[80px] rounded-full overflow-hidden relative">
|
<div className="w-[80px] h-[80px] rounded-full overflow-hidden relative">
|
||||||
{(!avatar || !avatarLoaded || isPlaceholder) && (
|
{(!avatar || !avatarLoaded || isPlaceholder) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src={placeholder}
|
|
||||||
alt={name}
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{avatar && !isPlaceholder && (
|
{avatar && !isPlaceholder && (
|
||||||
<img
|
<img
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt={name}
|
alt={name}
|
||||||
className={`absolute inset-0 w-full h-full object-cover rounded-full transition-opacity duration-200 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
|
className={`absolute inset-0 w-full h-full object-cover rounded-full transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||||
onLoad={() => setAvatarLoaded(true)}
|
onLoad={() => setAvatarLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default function AgentProfileView() {
|
|||||||
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 [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
const [testimonials, setTestimonials] = useState<{ id: string; text: string; author: string; role: string; rating: number }[]>([]);
|
const [testimonials, setTestimonials] = useState<{ id: string; text: string; author: string; role: string; rating: number }[]>([]);
|
||||||
@@ -284,13 +285,19 @@ export default function AgentProfileView() {
|
|||||||
<div className="relative w-[200px] lg:w-[260px]">
|
<div className="relative w-[200px] lg:w-[260px]">
|
||||||
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden bg-[#e8e8e8]">
|
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden bg-[#e8e8e8]">
|
||||||
{getProfileImageUrl() ? (
|
{getProfileImageUrl() ? (
|
||||||
/* eslint-disable-next-line @next/next/no-img-element */
|
<>
|
||||||
|
{!imageLoaded && (
|
||||||
|
<div className="absolute inset-0 shimmer-loading rounded-[15px]" />
|
||||||
|
)}
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
src={getProfileImageUrl()!}
|
src={getProfileImageUrl()!}
|
||||||
alt="Profile"
|
alt="Profile"
|
||||||
className="w-full h-full object-cover"
|
className={`w-full h-full object-cover transition-opacity duration-300 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||||
|
onLoad={() => setImageLoaded(true)}
|
||||||
onError={() => setImageError(true)}
|
onError={() => setImageError(true)}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -61,6 +61,43 @@ interface ProfileCardProps {
|
|||||||
resolvedAvatarUrl?: string | null;
|
resolvedAvatarUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Profile image with shimmer loading
|
||||||
|
function ProfileImage({ src, alt, className }: { src: string | null; alt: string; className?: string }) {
|
||||||
|
const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>(src ? 'loading' : 'error');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStatus(src ? 'loading' : 'error');
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative overflow-hidden bg-[#e8e8e8] ${className || ''}`}>
|
||||||
|
{status === 'loading' && (
|
||||||
|
<div className="absolute inset-0 shimmer-loading" />
|
||||||
|
)}
|
||||||
|
{src && status !== 'error' ? (
|
||||||
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={`w-full h-full object-cover transition-opacity duration-300 ${status === 'loaded' ? 'opacity-100' : 'opacity-0'}`}
|
||||||
|
onLoad={() => setStatus('loaded')}
|
||||||
|
onError={() => setStatus('error')}
|
||||||
|
/>
|
||||||
|
) : status === 'error' ? (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/user-placeholder-icon.svg"
|
||||||
|
alt={alt}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className="opacity-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
||||||
const [showFullBio, setShowFullBio] = useState(false);
|
const [showFullBio, setShowFullBio] = useState(false);
|
||||||
const [showAllTags, setShowAllTags] = useState(false);
|
const [showAllTags, setShowAllTags] = useState(false);
|
||||||
@@ -235,30 +272,11 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
|||||||
<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)]">
|
||||||
{/* 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">
|
||||||
<div className="relative w-full sm:w-[200px] h-[200px] rounded-[15px] overflow-hidden bg-[#e8e8e8]">
|
<ProfileImage
|
||||||
{getProfileImageUrl() ? (
|
src={getProfileImageUrl()}
|
||||||
/* eslint-disable-next-line @next/next/no-img-element */
|
|
||||||
<img
|
|
||||||
src={getProfileImageUrl()!}
|
|
||||||
alt={`${profile.firstName} ${profile.lastName}`}
|
alt={`${profile.firstName} ${profile.lastName}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full sm:w-[200px] h-[200px] rounded-[15px]"
|
||||||
onError={(e) => {
|
|
||||||
const target = e.target as HTMLImageElement;
|
|
||||||
target.style.display = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<Image
|
|
||||||
src="/assets/icons/user-placeholder-icon.svg"
|
|
||||||
alt={`${profile.firstName} ${profile.lastName}`}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="opacity-40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Link href={`/user/profile/${profile.id}`} className="mt-4">
|
<Link href={`/user/profile/${profile.id}`} className="mt-4">
|
||||||
<button className="w-[113px] py-2.5 bg-[#e58625] hover:bg-[#d47720] text-[#00293d] font-fractul font-bold text-[14px] rounded-[15px] transition-colors">
|
<button className="w-[113px] py-2.5 bg-[#e58625] hover:bg-[#d47720] text-[#00293d] font-fractul font-bold text-[14px] rounded-[15px] transition-colors">
|
||||||
View Profile
|
View Profile
|
||||||
|
|||||||
@@ -31,17 +31,24 @@ function ProfessionalCard({
|
|||||||
profileLink,
|
profileLink,
|
||||||
}: ProfessionalCardProps) {
|
}: ProfessionalCardProps) {
|
||||||
const starCount = rating ? Math.round(rating) : 0;
|
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 (
|
return (
|
||||||
<Link href={profileLink}>
|
<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">
|
<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 */}
|
{/* Image */}
|
||||||
<div className="relative h-[226px] w-full overflow-hidden bg-gray-100">
|
<div className="relative h-[226px] w-full overflow-hidden bg-gray-100">
|
||||||
|
{!imgLoaded && !isPlaceholder && (
|
||||||
|
<div className="absolute inset-0 shimmer-loading" />
|
||||||
|
)}
|
||||||
<Image
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={name}
|
alt={name}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className={`object-cover transition-opacity duration-300 ${isPlaceholder || imgLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||||
|
onLoad={() => setImgLoaded(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -203,20 +203,13 @@ export function CommonHeader() {
|
|||||||
{/* Avatar */}
|
{/* 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">
|
<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) && (
|
{(!userImage || !avatarLoaded) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src="/assets/icons/user-placeholder-icon.svg"
|
|
||||||
alt="Profile"
|
|
||||||
width={35}
|
|
||||||
height={35}
|
|
||||||
className="absolute inset-0 object-cover"
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{userImage && (
|
{userImage && (
|
||||||
<img
|
<img
|
||||||
src={userImage}
|
src={userImage}
|
||||||
alt="Profile"
|
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)}
|
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="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">
|
<div className="w-[42px] h-[42px] rounded-full overflow-hidden flex-shrink-0 bg-gray-100 relative">
|
||||||
{(!userImage || !avatarLoaded) && (
|
{(!userImage || !avatarLoaded) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src="/assets/icons/user-placeholder-icon.svg"
|
|
||||||
alt="Profile"
|
|
||||||
width={42}
|
|
||||||
height={42}
|
|
||||||
className="absolute inset-0 object-cover"
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{userImage && (
|
{userImage && (
|
||||||
<img
|
<img
|
||||||
src={userImage}
|
src={userImage}
|
||||||
alt="Profile"
|
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)}
|
onLoad={() => setAvatarLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -124,19 +124,13 @@ export function ChatHeader({
|
|||||||
<div className="relative flex-shrink-0">
|
<div className="relative flex-shrink-0">
|
||||||
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20 relative">
|
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20 relative">
|
||||||
{(!avatar || !avatarLoaded || isPlaceholderAvatar) && (
|
{(!avatar || !avatarLoaded || isPlaceholderAvatar) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src={placeholder}
|
|
||||||
alt={name}
|
|
||||||
width={50}
|
|
||||||
height={50}
|
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{avatar && !isPlaceholderAvatar && (
|
{avatar && !isPlaceholderAvatar && (
|
||||||
<img
|
<img
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt={name}
|
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)}
|
onLoad={() => setAvatarLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function formatLastSeen(lastSeenAt: string | null | undefined, isOnline: boolean
|
|||||||
return `Last seen ${diffDays}d ago`;
|
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 }) {
|
function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; size: number }) {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const placeholder = '/assets/icons/user-placeholder-icon.svg';
|
const placeholder = '/assets/icons/user-placeholder-icon.svg';
|
||||||
@@ -89,19 +89,13 @@ function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; s
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(!src || !loaded) && (
|
{(!src || !loaded) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src={placeholder}
|
|
||||||
alt={alt}
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{src && !isPlaceholder && (
|
{src && !isPlaceholder && (
|
||||||
<img
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
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)}
|
onLoad={() => setLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -792,8 +786,8 @@ export function MessagingPage() {
|
|||||||
onClick={() => window.open(messageFileUrls[message.id], '_blank')}
|
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="w-[200px] h-[150px] rounded-[12px] overflow-hidden">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#00293d]" />
|
<div className="w-full h-full shimmer-loading rounded-[12px]" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : message.messageType === 'FILE' ? (
|
) : message.messageType === 'FILE' ? (
|
||||||
|
|||||||
@@ -390,19 +390,13 @@ export function ProfileSettingsForm({
|
|||||||
<div className="flex items-start gap-4">
|
<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">
|
<div className="w-[60px] h-[60px] rounded-full overflow-hidden border border-[#00293D]/20 flex-shrink-0 bg-gray-100 relative">
|
||||||
{(!avatarUrl || !avatarLoaded) && (
|
{(!avatarUrl || !avatarLoaded) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src="/assets/icons/user-placeholder-icon.svg"
|
|
||||||
alt="Profile"
|
|
||||||
width={60}
|
|
||||||
height={60}
|
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{avatarUrl && (
|
{avatarUrl && (
|
||||||
<img
|
<img
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt="Profile"
|
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)}
|
onLoad={() => setAvatarLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -138,19 +138,13 @@ export function SettingsSidebar({
|
|||||||
<div className="flex items-center gap-4">
|
<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">
|
<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) && (
|
{(!profileData.avatarUrl || !avatarLoaded) && (
|
||||||
<Image
|
<div className="absolute inset-0 rounded-full shimmer-loading" />
|
||||||
src="/assets/icons/user-placeholder-icon.svg"
|
|
||||||
alt={profileData.name}
|
|
||||||
width={70}
|
|
||||||
height={70}
|
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{profileData.avatarUrl && (
|
{profileData.avatarUrl && (
|
||||||
<img
|
<img
|
||||||
src={profileData.avatarUrl}
|
src={profileData.avatarUrl}
|
||||||
alt={profileData.name}
|
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)}
|
onLoad={() => setAvatarLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user