● Zion Boggan
repos/Docket/components/member-avatar.tsx
zionboggan.com ↗
63 lines · tsx
History for this file →
1
"use client";
2
 
3
import { useState } from "react";
4
import { clsx } from "@/lib/cx";
5
 
6
 
7
export type AvatarSize = "sm" | "md" | "lg";
8
 
9
const SIZE_CLASS: Record<AvatarSize, string> = {
10
  sm: "h-8 w-8 text-[11px]",
11
  md: "h-11 w-11 text-[12px]",
12
  lg: "h-20 w-20 text-xl",
13
};
14
 
15
export function initialsOf(name: string): string {
16
  const cleaned = name.replace(/\(.*?\)/g, "").trim();
17
  const parts = cleaned.split(/\s+/).filter(Boolean);
18
  if (parts.length === 0) return "?";
19
  if (parts.length === 1) {
20
    return parts[0].slice(0, 2).toUpperCase();
21
  }
22
  const last = parts[parts.length - 1];
23
  return (parts[0][0] + (last[0] ?? "")).toUpperCase();
24
}
25
 
26
export function MemberAvatar({
27
  name,
28
  photoUrl,
29
  size = "sm",
30
  className,
31
}: {
32
  name: string;
33
  photoUrl?: string | null;
34
  size?: AvatarSize;
35
  className?: string;
36
}) {
37
  const [broken, setBroken] = useState(false);
38
  const showImg = !!photoUrl && !broken;
39
 
40
  return (
41
    <span
42
      aria-hidden="true"
43
      className={clsx(
44
        "relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full border border-line bg-graphite font-mono font-semibold uppercase tracking-tight text-ash",
45
        SIZE_CLASS[size],
46
        className
47
      )}
48
    >
49
      <span className="leading-none">{initialsOf(name)}</span>
50
      {showImg ? (
51
        <img
52
          src={photoUrl || undefined}
53
          alt=""
54
          loading="lazy"
55
          decoding="async"
56
          referrerPolicy="no-referrer"
57
          onError={() => setBroken(true)}
58
          className="absolute inset-0 h-full w-full object-cover"
59
        />
60
      ) : null}
61
    </span>
62
  );
63
}