Skip to content
133 linesCodeBlameRaw
1import { User } from "lucide-react";
2import { Link } from "react-router";
3
4import { type CommitPerson, type ShownCommit, profileHref, shownName } from "../lib/commit-people";
5import { cn } from "../lib/cn";
6
7import { UserCard } from "./user-card";
8import { Avatar } from "./ui/avatar";
9
10/**
11 * A person on a commit, as an avatar: theirs and their card when the
12 * commit is matched to an account (or is g1t's), else a plain one.
13 */
14export function CommitAvatar({ person, size = 20 }: { person: CommitPerson; size?: number }) {
15 if (person.kind === "user" || person.kind === "g1t") {
16 const href = profileHref(person);
17 const avatar = <Avatar name={shownName(person)} image={person.avatar} size={size} />;
18 return (
19 <UserCard username={person.username}>
20 {href ? (
21 // Its name beside it is the link a keyboard reaches; this is for the pointer.
22 <Link to={href} tabIndex={-1} aria-hidden="true" className="relative z-10 inline-flex shrink-0 rounded-full">
23 {avatar}
24 </Link>
25 ) : (
26 <span className="relative z-10 inline-flex shrink-0">{avatar}</span>
27 )}
28 </UserCard>
29 );
30 }
31 // Nobody on g1t, or a deleted account: a plain silhouette, no card.
32 return (
33 <span
34 aria-hidden="true"
35 className="inline-flex shrink-0 items-center justify-center rounded-full bg-line text-faint"
36 style={{ width: size, height: size }}
37 >
38 <User size={Math.round(size * 0.62)} strokeWidth={2.25} />
39 </span>
40 );
41}
42
43/**
44 * A commit's author and co-authors, as overlapping avatars: the author's
45 * on top, at most `max`.
46 */
47export function CommitAvatars({ commit, size = 20, max = 3 }: { commit: Pick<ShownCommit, "author" | "coAuthors">; size?: number; max?: number }) {
48 const people = [commit.author, ...commit.coAuthors].slice(0, max);
49 if (people.length === 1) return <CommitAvatar person={commit.author} size={size} />;
50 return (
51 <span className="inline-flex shrink-0 items-center">
52 {people.map((person, index) => (
53 <span
54 key={`${index}:${person.username ?? person.name}`}
55 className="inline-flex rounded-full ring-2 ring-bg"
56 style={{ marginLeft: index === 0 ? 0 : -Math.round(size * 0.35), zIndex: people.length - index }}
57 >
58 <CommitAvatar person={person} size={size} />
59 </span>
60 ))}
61 </span>
62 );
63}
64
65/**
66 * A person on a commit, by name: their username, linked to their profile
67 * with their card, for an account; g1t with its label and card; ghost for
68 * a deleted account; else the name written on the commit, as text.
69 */
70export function CommitName({ person, className }: { person: CommitPerson; className?: string }) {
71 if (person.kind === "g1t") {
72 return (
73 <UserCard username="g1t">
74 <span tabIndex={0} className="relative z-10 inline-flex items-baseline gap-1 rounded outline-none focus-visible:ring-2 focus-visible:ring-accent">
75 <span className={className}>g1t</span>
76 <span className="rounded border border-line px-1 text-[0.625rem] leading-[1.35] font-medium text-muted">bot</span>
77 </span>
78 </UserCard>
79 );
80 }
81 const href = profileHref(person);
82 if (!href) return <span className={className}>{shownName(person)}</span>;
83 return (
84 <UserCard username={person.username}>
85 <Link to={href} className={cn("relative z-10 hover:underline", className)}>
86 {shownName(person)}
87 </Link>
88 </UserCard>
89 );
90}
91
92/**
93 * Who made a commit, in words: the author, and its co-authors after them
94 * ("ada and grace", "ada, grace and 2 others").
95 */
96export function CommitNames({
97 commit,
98 className,
99 all,
100}: {
101 commit: Pick<ShownCommit, "author" | "coAuthors">;
102 className?: string;
103 /** Name every co-author, rather than counting past the first. */
104 all?: boolean;
105}) {
106 const others = commit.coAuthors;
107 if (others.length === 0) return <CommitName person={commit.author} className={className} />;
108 if (all && others.length > 1) {
109 const people = [commit.author, ...others];
110 return (
111 <>
112 {people.map((person, index) => (
113 <span key={index}>
114 {index > 0 && (index === people.length - 1 ? " and " : ", ")}
115 <CommitName person={person} className={className} />
116 </span>
117 ))}
118 </>
119 );
120 }
121 if (others.length === 1) {
122 return (
123 <>
124 <CommitName person={commit.author} className={className} /> and <CommitName person={others[0]!} className={className} />
125 </>
126 );
127 }
128 return (
129 <>
130 <CommitName person={commit.author} className={className} /> and {others.length} others
131 </>
132 );
133}