chore: code refactor
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user