import { useState } from "react"; import { useReducedMotion } from "~/hooks/use-reduced-motion"; import { useKnownAvatarKey } from "~/lib/avatar-version"; import { lavaLampDataUrl } from "~/lib/lava-lamp"; import { cn } from "~/utils/cn"; const AVATAR_PIXELS = { sm: 40, md: 64, lg: 160 } as const; const AVATAR_SIZES = { sm: "size-5", md: "size-8", lg: "size-20" } as const; type Props = { name: string; size?: keyof typeof AVATAR_PIXELS; src?: string | null }; /** * Shows an account's uploaded picture (looked up by name unless `src` is given) and falls back to * a lava lamp generated from the name when there is none or it fails to load. */ export function Avatar({ name, size = "md", src }: Props) { const [failedUrl, setFailedUrl] = useState(null); const [loadedUrl, setLoadedUrl] = useState(null); const knownKey = useKnownAvatarKey(name); const sizeClass = AVATAR_SIZES[size]; const byName = knownKey ? `/avatars/${knownKey}?s=${AVATAR_PIXELS[size]}` : `/avatars/u/${encodeURIComponent(name)}?s=${AVATAR_PIXELS[size]}`; const url = src === undefined ? byName : src; if (url && url !== failedUrl) { // A blank tile holds the spot until the picture loads, so a 404 never flashes a broken image before the lava lamp. const loaded = loadedUrl === url; return ( setLoadedUrl(url)} onError={() => setFailedUrl(url)} ref={(node) => { if (!node?.complete) return; if (node.naturalWidth === 0) setFailedUrl(url); else setLoadedUrl(url); }} className={cn("absolute inset-0 rounded-md object-cover", sizeClass, !loaded && "opacity-0")} /> ); } return ; } /** Static so it stays recognizable; drifts on hover, and always on the large profile avatar, unless motion is reduced. */ function LavaLampAvatar({ name, size }: { name: string; size: keyof typeof AVATAR_PIXELS }) { const reducedMotion = useReducedMotion(); const [hovered, setHovered] = useState(false); const animated = !reducedMotion && (hovered || size === "lg"); return ( setHovered(true)} onMouseLeave={() => setHovered(false)} className={cn("shrink-0 rounded-md bg-kumo-base object-cover", AVATAR_SIZES[size])} /> ); }