65 lines · 2.6 KB
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<string | null>(null);
  const [loadedUrl, setLoadedUrl] = useState<string | null>(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 (
      <span className={cn("relative inline-block shrink-0 rounded-md bg-kumo-base", sizeClass)}>
        <img
          src={url}
          alt=""
          loading="lazy"
          onLoad={() => setLoadedUrl(url)}
          onError={() => setFailedUrl(url)}
          ref={(node) => {
            if (!node?.complete) return;
            if (node.naturalWidth === 0) setFailedUrl(url);
            else setLoadedUrl(url);
          }}