diff --git a/src/components/auth/AuthButtons.tsx b/src/components/auth/AuthButtons.tsx index 9f1cf5c2..c476bcaf 100644 --- a/src/components/auth/AuthButtons.tsx +++ b/src/components/auth/AuthButtons.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { UserAvatar } from '@/components/ui/user-avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { User, Settings, LogOut } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; @@ -71,6 +71,7 @@ export function AuthButtons() { return diff --git a/src/components/ui/user-avatar.tsx b/src/components/ui/user-avatar.tsx new file mode 100644 index 00000000..2db6ec2e --- /dev/null +++ b/src/components/ui/user-avatar.tsx @@ -0,0 +1,96 @@ +import { useState, useEffect } from 'react'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; + +interface UserAvatarProps { + avatarUrl?: string | null; + fallbackText: string; + className?: string; + size?: 'sm' | 'md' | 'lg'; +} + +const sizeClasses = { + sm: 'h-8 w-8 text-xs', + md: 'h-10 w-10 text-sm', + lg: 'h-16 w-16 text-lg' +}; + +export function UserAvatar({ + avatarUrl, + fallbackText, + className, + size = 'md' +}: UserAvatarProps) { + const [imageUrl, setImageUrl] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + + const MAX_RETRIES = 2; + + // Reset state when avatarUrl changes + useEffect(() => { + if (avatarUrl) { + setImageUrl(avatarUrl); + setRetryCount(0); + setIsLoading(true); + setHasError(false); + } else { + setImageUrl(null); + setIsLoading(false); + setHasError(false); + } + }, [avatarUrl]); + + const handleImageError = () => { + console.warn('[UserAvatar] Image load failed:', imageUrl, 'Retry count:', retryCount); + + if (retryCount < MAX_RETRIES && avatarUrl) { + // Add cache-busting parameter and retry + const cacheBuster = `?retry=${retryCount + 1}&t=${Date.now()}`; + const urlWithCacheBuster = avatarUrl.includes('?') + ? `${avatarUrl}&retry=${retryCount + 1}&t=${Date.now()}` + : `${avatarUrl}${cacheBuster}`; + + console.log('[UserAvatar] Retrying with cache buster:', urlWithCacheBuster); + setRetryCount(prev => prev + 1); + setImageUrl(urlWithCacheBuster); + } else { + // All retries exhausted, show fallback + console.warn('[UserAvatar] All retries exhausted, showing fallback'); + setHasError(true); + setIsLoading(false); + } + }; + + const handleImageLoad = () => { + console.log('[UserAvatar] Image loaded successfully:', imageUrl); + setIsLoading(false); + setHasError(false); + }; + + const fallbackInitial = fallbackText.charAt(0).toUpperCase(); + + return ( + + {imageUrl && !hasError && ( + + )} + + {isLoading ? ( +
+ {fallbackInitial} +
+ ) : ( + fallbackInitial + )} +
+
+ ); +}