Files
AgentHub/client/src/components/users/UserAvatar.tsx
T
nntrivi2001 f1a90e120e fix(users): every user row shows its avatar, not only its initials
System Health's per-user CPU/RAM card asked UserAvatar for the viewer's own
picture with no user id, and the host stats never carried avatarAt, so every
card fell back to letters. The groups now carry avatarAt and the card passes
userId. The user editor's header, the pool board's member chips and the pool
member picker drew hand-made initials; they use UserAvatar too.
2026-10-29 13:40:28 +07:00

62 lines
2.0 KiB
TypeScript

/**
* @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, Settings → Account, the
* Users page and its editor, the pool board and member picker, and System
* Health's per-user CPU/RAM card.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
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<string | null>(null);
const base = `flex flex-shrink-0 items-center justify-center overflow-hidden rounded-full ${className}`;
if (url && failed !== url) {
return (
<img
src={url}
alt=""
aria-hidden
draggable={false}
onError={() => setFailed(url)}
className={`${base} object-cover`}
data-testid="user-avatar-img"
/>
);
}
return (
<span
aria-hidden
className={`${base} ${initialsClassName} font-semibold uppercase`}
data-testid="user-avatar-initials"
>
{name.slice(0, letters)}
</span>
);
}