/** * @file UserAvatar.tsx * @description An account's round picture: the signed-in account's own * (`GET /api/me/avatar`) or, with `userId`, another's (`GET * /api/users/:id/avatar`, staff only), versioned by `avatarAt`; without one — * or when the image fails to load — the first letters of its name. Used by the * sidebar's account row, its workspace owner groups and Settings → Account. * @author Nguyễn Ngọc Trí Vĩ */ import { useState } from "react"; import { api } from "../../lib/api"; export function UserAvatar({ name, avatarAt, userId, letters = 2, className = "h-7 w-7 text-[11px]", initialsClassName = "bg-accent/15 text-accent", }: { name: string; avatarAt?: string | null; /** Another account's (staff only); omitted = the signed-in account's own. */ userId?: number; /** How many letters of the name stand in for a missing picture. */ letters?: number; /** Size and font size: the parts that differ between call sites. */ className?: string; /** The initials' colours. */ initialsClassName?: string; }) { const url = userId != null ? api.users.avatarUrl(userId, avatarAt) : api.me.avatarUrl(avatarAt); // A broken image falls back to the initials, per URL. const [failed, setFailed] = useState(null); const base = `flex flex-shrink-0 items-center justify-center overflow-hidden rounded-full ${className}`; if (url && failed !== url) { return ( setFailed(url)} className={`${base} object-cover`} data-testid="user-avatar-img" /> ); } return ( {name.slice(0, letters)} ); }