import React, { useEffect, useState } from 'react'; import { cn } from '../../utils/classNames'; interface AvatarProps { name: string; imageUrl?: string; /** Diameter in pixels. */ size?: number; className?: string; } const getInitials = (name: string) => { const words = name.trim().split(/\s+/).filter(Boolean); if (words.length === 0) return '?'; if (words.length === 1) return words[0].charAt(0).toUpperCase(); return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase(); }; /** * User account avatar. Initials on a soft cyan duotone are always rendered as the * base; if an `imageUrl` is provided it crossfades in once it has actually loaded * (and is ignored on error), so the photo never "pops" over an empty circle and * there's no layout shift. */ const Avatar: React.FC = ({ name, imageUrl, size = 40, className }) => { const [imageReady, setImageReady] = useState(false); // Reset when the source changes (e.g. switching accounts). useEffect(() => { setImageReady(false); }, [imageUrl]); return ( {getInitials(name)} {imageUrl && ( setImageReady(true)} onError={() => setImageReady(false)} className={cn( 'absolute inset-0 h-full w-full rounded-full object-cover transition-opacity duration-300', imageReady ? 'opacity-100' : 'opacity-0', )} /> )} ); }; export default Avatar;