Skip to content

Commit 3ca2631

Browse files
feat(web): replace placeholder avatars with minidenticon-based UserAvatar component
Adds the minidenticons library and a new UserAvatar component that generates deterministic avatar icons from email addresses. Replaces all placeholder avatar usage across chat, settings, and redeem pages with this unified component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 331f025 commit 3ca2631

File tree

13 files changed

+118
-90
lines changed

13 files changed

+118
-90
lines changed

packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
"linguist-languages": "^9.3.1",
152152
"lucide-react": "^0.517.0",
153153
"micromatch": "^4.0.8",
154+
"minidenticons": "^4.2.1",
154155
"next": "16.1.6",
155156
"next-auth": "^5.0.0-beta.30",
156157
"next-navigation-guard": "^0.2.0",

packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { prisma } from '@/prisma';
44
import { getOrgFromDomain } from '@/data/org';
55
import { ChatVisibility } from '@sourcebot/db';
66
import { env } from "@sourcebot/shared";
7+
import { minidenticon } from 'minidenticons';
78

89
export const runtime = 'nodejs';
910
export const alt = 'Sourcebot Chat';
@@ -37,6 +38,7 @@ export default async function Image({ params }: ImageProps) {
3738
createdBy: {
3839
select: {
3940
name: true,
41+
email: true,
4042
image: true,
4143
},
4244
},
@@ -53,7 +55,9 @@ export default async function Image({ params }: ImageProps) {
5355
const chatName = rawChatName.length > MAX_CHAT_NAME_LENGTH
5456
? rawChatName.substring(0, MAX_CHAT_NAME_LENGTH).trim() + '...'
5557
: rawChatName;
56-
const creatorImage = chat.createdBy?.image;
58+
const creatorEmail = chat.createdBy?.email;
59+
const creatorImage = chat.createdBy?.image
60+
?? (creatorEmail ? 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(creatorEmail, 50, 50)) : undefined);
5761

5862
return new ImageResponse(
5963
(

packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
import { searchChatShareableMembers } from "@/app/api/(client)/client";
44
import { SearchChatShareableMembersResponse } from "@/app/api/(server)/ee/chat/[chatId]/searchMembers/route";
55
import { SessionUser } from "@/auth";
6-
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
76
import { Badge } from "@/components/ui/badge";
87
import { Button } from "@/components/ui/button";
98
import { LoadingButton } from "@/components/ui/loading-button";
109
import { Separator } from "@/components/ui/separator";
1110
import { unwrapServiceError } from "@/lib/utils";
12-
import placeholderAvatar from "@/public/placeholder_avatar.png";
11+
import { UserAvatar } from "@/components/userAvatar";
1312
import { useQuery } from "@tanstack/react-query";
1413
import { useDebounce } from "@uidotdev/usehooks";
1514
import { ChevronLeft, Circle, CircleCheck, Loader2, X } from "lucide-react";
@@ -33,17 +32,6 @@ export const InvitePanel = ({
3332
const resultsRef = useRef<HTMLDivElement>(null);
3433
const inputRef = useRef<HTMLInputElement>(null);
3534

36-
const getInitials = (name?: string, email?: string) => {
37-
if (name) {
38-
return name.split(' ').map((n) => n[0]).join('').toUpperCase().slice(0, 2);
39-
}
40-
if (email) {
41-
return email[0].toUpperCase();
42-
}
43-
return '?';
44-
};
45-
46-
4735
const debouncedSearchQuery = useDebounce(searchQuery, 100);
4836

4937
const { data: searchResults, isPending, isError } = useQuery<SearchChatShareableMembersResponse>({
@@ -157,10 +145,11 @@ export const InvitePanel = ({
157145
) : (
158146
<Circle className="h-5 w-5 text-muted-foreground shrink-0" />
159147
)}
160-
<Avatar className="h-8 w-8 ml-2">
161-
<AvatarImage src={user.image ?? placeholderAvatar.src} />
162-
<AvatarFallback>{getInitials(user.name ?? undefined, user.email ?? undefined)}</AvatarFallback>
163-
</Avatar>
148+
<UserAvatar
149+
email={user.email}
150+
imageUrl={user.image}
151+
className="h-8 w-8 ml-2"
152+
/>
164153
<div className="flex flex-col items-start ml-1">
165154
<span className="text-sm font-medium">{user.name || user.email}</span>
166155
{user.name && (

packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { SessionUser } from "@/auth";
44
import { useToast } from "@/components/hooks/use-toast";
5-
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
65
import { Button } from "@/components/ui/button";
76
import {
87
Select,
@@ -13,7 +12,7 @@ import {
1312
} from "@/components/ui/select";
1413
import { Separator } from "@/components/ui/separator";
1514
import { cn } from "@/lib/utils";
16-
import placeholderAvatar from "@/public/placeholder_avatar.png";
15+
import { UserAvatar } from "@/components/userAvatar";
1716
import { ChatVisibility } from "@sourcebot/db";
1817
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
1918
import { Info, Link2Icon, Loader2, Lock, X } from "lucide-react";
@@ -69,16 +68,6 @@ export const ShareSettings = ({
6968
}
7069
}, [chatId, visibility, toast]);
7170

72-
const getInitials = (name?: string | null, email?: string | null) => {
73-
if (name) {
74-
return name.split(' ').map((n) => n[0]).join('').toUpperCase().slice(0, 2);
75-
}
76-
if (email) {
77-
return email[0].toUpperCase();
78-
}
79-
return '?';
80-
};
81-
8271
return (
8372
<div className="flex flex-col py-3 px-4">
8473
<p className="text-sm font-medium">Share</p>
@@ -113,10 +102,11 @@ export const ShareSettings = ({
113102
{currentUser && (
114103
<div className="flex items-center justify-between py-2">
115104
<div className="flex items-center gap-3">
116-
<Avatar className="h-8 w-8">
117-
<AvatarImage src={currentUser.image ?? placeholderAvatar.src} />
118-
<AvatarFallback>{getInitials(currentUser.name, currentUser.email)}</AvatarFallback>
119-
</Avatar>
105+
<UserAvatar
106+
email={currentUser.email}
107+
imageUrl={currentUser.image}
108+
className="h-8 w-8"
109+
/>
120110
<div className="flex flex-col">
121111
<span className="text-sm font-medium">
122112
{currentUser.name || currentUser.email}
@@ -134,10 +124,11 @@ export const ShareSettings = ({
134124
{sharedWithUsers.map((user) => (
135125
<div key={user.id} className="flex items-center justify-between py-2">
136126
<div className="flex items-center gap-3">
137-
<Avatar className="h-8 w-8">
138-
<AvatarImage src={user.image ?? placeholderAvatar.src} />
139-
<AvatarFallback>{getInitials(user.name, user.email)}</AvatarFallback>
140-
</Avatar>
127+
<UserAvatar
128+
email={user.email}
129+
imageUrl={user.image}
130+
className="h-8 w-8"
131+
/>
141132
<div className="flex flex-col">
142133
<span className="text-sm font-medium">{user.name || user.email}</span>
143134
{user.name && (

packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ import {
1313
DropdownMenuTrigger,
1414
} from "@/components/ui/dropdown-menu"
1515
import { cn } from "@/lib/utils"
16-
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
1716
import { signOut } from "next-auth/react"
1817
import posthog from "posthog-js";
1918
import { useDomain } from "@/hooks/useDomain";
2019
import { Session } from "next-auth";
2120
import { AppearanceDropdownMenuGroup } from "./appearanceDropdownMenuGroup";
22-
import placeholderAvatar from "@/public/placeholder_avatar.png";
21+
import { UserAvatar } from "@/components/userAvatar";
2322

2423
interface MeControlDropdownMenuProps {
2524
menuButtonClassName?: string;
@@ -35,24 +34,20 @@ export const MeControlDropdownMenu = ({
3534
return (
3635
<DropdownMenu>
3736
<DropdownMenuTrigger asChild>
38-
<Avatar className={cn("h-8 w-8 cursor-pointer", menuButtonClassName)}>
39-
<AvatarImage src={session.user.image ?? placeholderAvatar.src} />
40-
<AvatarFallback className="bg-primary/10 text-primary font-semibold text-sm">
41-
{session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'}
42-
</AvatarFallback>
43-
</Avatar>
37+
<UserAvatar
38+
email={session.user.email}
39+
imageUrl={session.user.image}
40+
className={cn("h-8 w-8 cursor-pointer", menuButtonClassName)}
41+
/>
4442
</DropdownMenuTrigger>
4543
<DropdownMenuContent className="w-64" align="end" sideOffset={5}>
4644
<DropdownMenuGroup>
4745
<div className="flex flex-row items-center gap-3 px-3 py-3">
48-
<Avatar className="h-10 w-10 flex-shrink-0">
49-
<AvatarImage
50-
src={session.user.image ?? placeholderAvatar.src}
51-
/>
52-
<AvatarFallback className="bg-primary/10 text-primary font-semibold">
53-
{session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'}
54-
</AvatarFallback>
55-
</Avatar>
46+
<UserAvatar
47+
email={session.user.email}
48+
imageUrl={session.user.image}
49+
className="h-10 w-10 flex-shrink-0"
50+
/>
5651
<div className="flex flex-col flex-1 min-w-0">
5752
<p className="text-sm font-semibold truncate">{session.user.name ?? "User"}</p>
5853
{session.user.email && (

packages/web/src/app/[domain]/settings/members/components/invitesList.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
import { OrgRole } from "@sourcebot/db";
44
import { useToast } from "@/components/hooks/use-toast";
55
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
6-
import { Avatar, AvatarImage } from "@/components/ui/avatar";
76
import { Button } from "@/components/ui/button";
87
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
98
import { Input } from "@/components/ui/input";
109
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
1110
import { createPathWithQueryParams, isServiceError } from "@/lib/utils";
12-
import placeholderAvatar from "@/public/placeholder_avatar.png";
11+
import { UserAvatar } from "@/components/userAvatar";
1312
import { Copy, MoreVertical, Search } from "lucide-react";
1413
import { useCallback, useMemo, useState } from "react";
1514
import { cancelInvite } from "@/actions";
@@ -109,9 +108,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => {
109108
filteredInvites.map((invite) => (
110109
<div key={invite.id} className="p-4 flex items-center justify-between bg-background">
111110
<div className="flex items-center gap-3">
112-
<Avatar>
113-
<AvatarImage src={placeholderAvatar.src} />
114-
</Avatar>
111+
<UserAvatar email={invite.email} />
115112
<div>
116113
<div className="text-sm text-muted-foreground">{invite.email}</div>
117114
</div>

packages/web/src/app/[domain]/settings/members/components/membersList.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import { Input } from "@/components/ui/input";
44
import { Search, MoreVertical } from "lucide-react";
55
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
6-
import { Avatar, AvatarImage } from "@/components/ui/avatar";
76
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
87
import { Button } from "@/components/ui/button";
98
import { useCallback, useMemo, useState } from "react";
109
import { OrgRole } from "@prisma/client";
11-
import placeholderAvatar from "@/public/placeholder_avatar.png";
10+
import { UserAvatar } from "@/components/userAvatar";
1211
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
1312
import { promoteToOwner, demoteToMember } from "@/ee/features/userManagement/actions";
1413
import { leaveOrg, removeMemberFromOrg } from "@/features/userManagement/actions";
@@ -200,9 +199,10 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName,
200199
filteredMembers.map((member) => (
201200
<div key={member.id} className="p-4 flex items-center justify-between bg-background">
202201
<div className="flex items-center gap-3">
203-
<Avatar>
204-
<AvatarImage src={member.avatarUrl ?? placeholderAvatar.src} />
205-
</Avatar>
202+
<UserAvatar
203+
email={member.email}
204+
imageUrl={member.avatarUrl}
205+
/>
206206
<div>
207207
<div className="font-medium">{member.name}</div>
208208
<div className="text-sm text-muted-foreground">{member.email}</div>

packages/web/src/app/[domain]/settings/members/components/requestsList.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import { OrgRole } from "@sourcebot/db";
44
import { useToast } from "@/components/hooks/use-toast";
55
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
6-
import { Avatar, AvatarImage } from "@/components/ui/avatar";
76
import { Button } from "@/components/ui/button";
87
import { Input } from "@/components/ui/input";
98
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
109
import { isServiceError } from "@/lib/utils";
11-
import placeholderAvatar from "@/public/placeholder_avatar.png";
10+
import { UserAvatar } from "@/components/userAvatar";
1211
import { CheckCircle, Search, XCircle } from "lucide-react";
1312
import { useCallback, useMemo, useState } from "react";
1413
import { approveAccountRequest, rejectAccountRequest } from "@/actions";
@@ -132,9 +131,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) =
132131
filteredRequests.map((request) => (
133132
<div key={request.id} className="p-4 flex items-center justify-between bg-background">
134133
<div className="flex items-center gap-3">
135-
<Avatar>
136-
<AvatarImage src={placeholderAvatar.src} />
137-
</Avatar>
134+
<UserAvatar email={request.email} />
138135
<div>
139136
<div className="font-medium">{request.name || request.email}</div>
140137
<div className="text-sm text-muted-foreground">{request.email}</div>

packages/web/src/app/redeem/components/acceptInviteCard.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
44
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
55
import Link from "next/link";
6-
import { Avatar, AvatarImage } from "@/components/ui/avatar";
7-
import placeholderAvatar from "@/public/placeholder_avatar.png";
6+
import { UserAvatar } from "@/components/userAvatar";
87
import { ArrowRight, Loader2 } from "lucide-react";
98
import { Button } from "@/components/ui/button";
109
import { useCallback, useState } from "react";
@@ -74,13 +73,17 @@ export const AcceptInviteCard = ({ inviteId, orgName, orgDomain, orgImageUrl, ho
7473
<InvitedByText email={host.email} name={host.name} /> invited you to join the <strong>{orgName}</strong> organization.
7574
</p>
7675
<div className="flex fex-row items-center justify-center gap-2 mt-12">
77-
<Avatar className="w-14 h-14">
78-
<AvatarImage src={host.avatarUrl ?? placeholderAvatar.src} />
79-
</Avatar>
76+
<UserAvatar
77+
email={host.email}
78+
imageUrl={host.avatarUrl}
79+
className="w-14 h-14"
80+
/>
8081
<ArrowRight className="w-4 h-4 text-muted-foreground" />
81-
<Avatar className="w-14 h-14">
82-
<AvatarImage src={orgImageUrl ?? placeholderAvatar.src} />
83-
</Avatar>
82+
<UserAvatar
83+
email={orgName}
84+
imageUrl={orgImageUrl}
85+
className="w-14 h-14"
86+
/>
8487
</div>
8588
<Button
8689
className="mt-12 mx-auto w-full"

packages/web/src/app/redeem/components/inviteNotFoundCard.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
2-
import { Avatar, AvatarImage } from "@/components/ui/avatar";
3-
import placeholderAvatar from "@/public/placeholder_avatar.png";
2+
import { UserAvatar } from "@/components/userAvatar";
43
import { auth } from "@/auth";
54
import { Card } from "@/components/ui/card";
65

@@ -19,9 +18,11 @@ export const InviteNotFoundCard = async () => {
1918
The invite you are trying to redeem has already been used, expired, or does not exist.
2019
</p>
2120
<div className="flex flex-col items-center gap-2 mt-8">
22-
<Avatar className="h-12 w-12">
23-
<AvatarImage src={session?.user.image ?? placeholderAvatar.src} />
24-
</Avatar>
21+
<UserAvatar
22+
email={session?.user.email}
23+
imageUrl={session?.user.image}
24+
className="h-12 w-12"
25+
/>
2526
<p className="text-sm text-muted-foreground">
2627
Logged in as <strong>{session?.user?.email}</strong>
2728
</p>

0 commit comments

Comments
 (0)