Files
2026-06-13 02:24:42 +05:30

102 lines
2.9 KiB
TypeScript

import Image from "next/image";
import { cn } from "@/lib/utils";
/**
* UserAvatar — the one avatar component: shows the user's image, or their
* initials on a tinted disc when there's no image. Used everywhere a user
* thumbnail appears (notification feed, comments, follower lists, and the
* profile header's large zoomable hero).
*
* Sizing is class-driven (`className`) rather than a fixed px, so callers get
* Tailwind's responsive sizes — e.g. `size-10` for a list row or
* `size-24 sm:size-28` for the profile header. `textClassName` scales the
* initials to match. When `onClick` is set, an image avatar becomes a
* focusable button (used by the profile header to open its zoom preview);
* `interactive` adds a hover-ring affordance.
*/
export function UserAvatar({
src,
name,
className,
textClassName = "text-sm",
onClick,
ariaLabel,
interactive = false,
}: {
src?: string | null;
name: string;
/** Box size + extras. Defaults to a 40px (size-10) circle. */
className?: string;
/** Initials font size — match it to the box. Defaults to text-sm. */
textClassName?: string;
/** When set, an image avatar renders as a focusable button (e.g. to open a
* zoom preview or navigate). Initials stay non-interactive. */
onClick?: () => void;
ariaLabel?: string;
/** Add a hover-ring affordance to an image avatar. */
interactive?: boolean;
}) {
const size = className ?? "size-10";
if (src) {
const imgRing = cn(
"rounded-full object-cover ring-1 ring-foreground/10",
interactive && "hover:ring-foreground/30 transition",
);
// width/height satisfy next/image; the rendered size comes from the CSS
// size class (which overrides the intrinsic attributes).
if (onClick) {
return (
<button
type="button"
onClick={onClick}
aria-label={ariaLabel ?? `View ${name}'s picture`}
className={cn(
"shrink-0 rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
size,
)}
>
<Image
src={src}
alt={name}
width={112}
height={112}
className={cn("size-full", imgRing)}
unoptimized
/>
</button>
);
}
return (
<Image
src={src}
alt={name}
width={112}
height={112}
className={cn("shrink-0", size, imgRing)}
unoptimized
/>
);
}
const initials =
name
.replace(/^@/, "")
.split(/\s+/)
.map((p) => p[0])
.slice(0, 2)
.join("")
.toUpperCase() || "?";
return (
<div
className={cn(
"shrink-0 rounded-full bg-primary/15 text-primary flex items-center justify-center font-semibold ring-1 ring-foreground/10",
size,
textClassName,
)}
>
{initials}
</div>
);
}