Show user avatars in the most-active-users leaderboard

Original task #33: The admin "Most active users" leaderboard rendered
only a colored circle with the first letter of each user's name even
though the API already returns each user's avatarUrl.

Changes:
- artifacts/teaboy-os/src/pages/admin.tsx
  - Added a small LeaderboardAvatar component that renders the avatar
    image when available and falls back to the initial when there is
    no avatarUrl or when the image fails to load (onError).
  - The avatar URL is resolved through the existing
    resolveServiceImageUrl helper (consistent with chat.tsx).
  - Kept the existing emerald circle styling as the fallback
    background; added overflow-hidden so the rounded image is clipped.
  - useEffect resets the error state when the src changes so a later
    valid URL still attempts to load.

No API or schema changes were required (avatarUrl is already returned
by GET /admin/stats and present in the generated client types).

Pre-existing TypeScript errors in admin.tsx / chat.tsx / clock files
are unrelated to this task and were not touched.

Replit-Task-Id: a0fe21b3-7422-48ba-9731-ed8315eb9d68
This commit is contained in:
riyadhafraa
2026-04-21 08:54:14 +00:00
parent 13040c8a93
commit 1e6713f531
+32 -3
View File
@@ -41,6 +41,36 @@ import { formatDate as formatDateUtil, formatWeekday as formatWeekdayUtil } from
type AdminSection = "dashboard" | "apps" | "services" | "users" | "settings";
function LeaderboardAvatar({
src,
initial,
alt,
}: {
src: string | null;
initial: string;
alt: string;
}) {
const [errored, setErrored] = useState(false);
useEffect(() => {
setErrored(false);
}, [src]);
const showImage = src && !errored;
return (
<div className="w-8 h-8 rounded-full shrink-0 bg-emerald-100/70 text-emerald-700 flex items-center justify-center text-xs font-semibold overflow-hidden">
{showImage ? (
<img
src={src}
alt={alt}
onError={() => setErrored(true)}
className="w-full h-full object-cover"
/>
) : (
initial
)}
</div>
);
}
type AppForm = {
nameAr: string;
nameEn: string;
@@ -1353,6 +1383,7 @@ function DashboardSection({
const displayName =
(lang === "ar" ? u.displayNameAr : u.displayNameEn) ?? u.username;
const initial = (displayName || u.username || "?").trim().charAt(0).toUpperCase();
const avatarSrc = resolveServiceImageUrl(u.avatarUrl);
return (
<li key={u.userId}>
<button
@@ -1364,9 +1395,7 @@ function DashboardSection({
<div className="text-xs font-semibold text-muted-foreground w-5 text-center shrink-0">
{idx + 1}
</div>
<div className="w-8 h-8 rounded-full shrink-0 bg-emerald-100/70 text-emerald-700 flex items-center justify-center text-xs font-semibold">
{initial}
</div>
<LeaderboardAvatar src={avatarSrc} initial={initial} alt={displayName} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">