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