Skip to content

Commit 5446b9e

Browse files
authored
Merge pull request #937 from amitamrutiya/fix-collab
Add collaborator avatar group component
2 parents 2e32cab + c3c6a4a commit 5446b9e

File tree

3 files changed

+214
-0
lines changed

3 files changed

+214
-0
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { ExpandMore } from '@mui/icons-material';
2+
import { MouseEvent, useState } from 'react';
3+
import { Avatar, AvatarGroup, Popover, Typography } from '../../base';
4+
import { iconSmall } from '../../constants/iconsSizes';
5+
import { styled, useTheme } from '../../theme';
6+
import { DARK_TEAL_BLUE } from '../../theme/colors/colors';
7+
import { CustomTooltip } from '../CustomTooltip';
8+
9+
/**
10+
* CollaboratorAvatarGroup is a component that displays a group of user avatars with a popup for additional users.
11+
*
12+
* @component
13+
* @example
14+
* ```tsx
15+
* const users = {
16+
* 'client1': {
17+
* name: 'John Doe',
18+
* avatar_url: 'https://example.com/avatar1.jpg',
19+
* border_color: '#00B39F',
20+
* user_id: 'user123'
21+
* }
22+
* };
23+
*
24+
* <CollaboratorAvatarGroup
25+
* users={users}
26+
* providerUrl="https://redirect.com"
27+
* />
28+
* ```
29+
*/
30+
31+
/**
32+
* User object structure representing a collaborator
33+
* @interface User
34+
* @property {string} name - Display name of the user
35+
* @property {string} avatar_url - URL to the user's avatar image
36+
* @property {string} border_color - Color code for the avatar border (e.g., '#00B39F')
37+
* @property {string} user_id - Unique identifier for the user
38+
*/
39+
interface User {
40+
name: string;
41+
avatar_url: string;
42+
border_color: string;
43+
user_id: string;
44+
}
45+
46+
/**
47+
* Collection of users mapped by their client IDs
48+
* @interface Users
49+
* @property {User} [clientID] - User object mapped to their client ID
50+
*/
51+
interface Users {
52+
[clientID: string]: User;
53+
}
54+
55+
/**
56+
* Props for the CollaboratorAvatarGroup component
57+
* @interface CollaboratorAvatarGroupProps
58+
* @property {Users} users - Object containing user information mapped by client IDs
59+
* @property {string} providerUrl - Base URL of the provider (e.g., 'https://github.com')
60+
*/
61+
interface CollaboratorAvatarGroupProps {
62+
users: Users;
63+
providerUrl: string;
64+
}
65+
66+
interface StyledAvatarProps {
67+
borderColor: string;
68+
}
69+
const StyledAvatar = styled(Avatar)<StyledAvatarProps>(({ theme, borderColor }) => {
70+
return {
71+
width: theme.spacing(4),
72+
height: theme.spacing(4),
73+
cursor: 'pointer',
74+
border: `1.5px solid ${borderColor || theme.palette.common.white} !important`
75+
};
76+
});
77+
78+
const MoreAvatarButton = styled('div')(({ theme }) => ({
79+
width: theme.spacing(4.5),
80+
height: theme.spacing(4.5),
81+
border: `1.5px solid ${theme.palette.common.white}`,
82+
borderRadius: '50%',
83+
background: DARK_TEAL_BLUE,
84+
display: 'flex',
85+
justifyContent: 'center',
86+
alignItems: 'center',
87+
marginLeft: '-10px',
88+
zIndex: 0,
89+
'&:hover': {
90+
cursor: 'pointer'
91+
}
92+
}));
93+
94+
const PopupAvatarWrapper = styled('div')({
95+
display: 'flex',
96+
alignItems: 'center',
97+
padding: '5px 15px 5px 10px',
98+
'&:hover': {
99+
cursor: 'pointer',
100+
background: '#cecece80 !important'
101+
}
102+
});
103+
104+
const UserName = styled(Typography)({
105+
marginLeft: '10px'
106+
});
107+
108+
const StyledPopover = styled(Popover)(() => ({
109+
'& .MuiPopover-paper': {
110+
marginTop: '10px',
111+
maxHeight: '331px'
112+
}
113+
}));
114+
115+
const CollaboratorAvatarGroup = ({
116+
users,
117+
providerUrl
118+
}: CollaboratorAvatarGroupProps): JSX.Element => {
119+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
120+
121+
const openInNewTab = (url: string): void => {
122+
window.open(url, '_blank', 'noreferrer');
123+
};
124+
125+
const handleClick = (event: MouseEvent<HTMLDivElement>): void => {
126+
setAnchorEl(event.currentTarget);
127+
};
128+
129+
const handleClose = (): void => {
130+
setAnchorEl(null);
131+
};
132+
133+
const totalUsers = Object.entries(users).length;
134+
const visibleAvatars = 4;
135+
const theme = useTheme();
136+
return (
137+
<AvatarGroup max={visibleAvatars + 1}>
138+
{Object.entries(users)
139+
.slice(0, visibleAvatars)
140+
.map(([clientID, user]) => {
141+
return (
142+
<CustomTooltip key={clientID} title={user.name} arrow>
143+
<StyledAvatar
144+
key={clientID}
145+
alt={user.name}
146+
src={user.avatar_url}
147+
borderColor={user.border_color}
148+
imgProps={{ referrerPolicy: 'no-referrer' }}
149+
onClick={() => openInNewTab(`${providerUrl}/user/${user.user_id}`)}
150+
/>
151+
</CustomTooltip>
152+
);
153+
})}
154+
{totalUsers > visibleAvatars && (
155+
<>
156+
<MoreAvatarButton onClick={handleClick} aria-describedby="user-popover">
157+
{anchorEl ? (
158+
<ExpandMore
159+
fill={theme.palette.common.white}
160+
{...iconSmall}
161+
style={{ marginLeft: '4px' }}
162+
/>
163+
) : (
164+
<Typography
165+
variant="body2"
166+
style={{ color: theme.palette.common.white, fontSize: '12px' }}
167+
>
168+
{`+${totalUsers - visibleAvatars}`}
169+
</Typography>
170+
)}
171+
</MoreAvatarButton>
172+
<StyledPopover
173+
id="user-popover"
174+
open={Boolean(anchorEl)}
175+
anchorEl={anchorEl}
176+
onClose={handleClose}
177+
anchorOrigin={{
178+
vertical: 'bottom',
179+
horizontal: 'left'
180+
}}
181+
transformOrigin={{
182+
vertical: 'top',
183+
horizontal: 'left'
184+
}}
185+
>
186+
{Object.entries(users)
187+
.slice(visibleAvatars, totalUsers)
188+
.map(([clientID, user]) => (
189+
<PopupAvatarWrapper
190+
key={clientID}
191+
onClick={() => openInNewTab(`${providerUrl}/user/${user.user_id}`)}
192+
>
193+
<StyledAvatar
194+
alt={user.name}
195+
src={user.avatar_url}
196+
borderColor={user.border_color}
197+
imgProps={{ referrerPolicy: 'no-referrer' }}
198+
/>
199+
<UserName variant="body1">{user.name}</UserName>
200+
</PopupAvatarWrapper>
201+
))}
202+
</StyledPopover>
203+
</>
204+
)}
205+
</AvatarGroup>
206+
);
207+
};
208+
209+
export default CollaboratorAvatarGroup;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import CollaboratorAvatarGroup from './CollaboratorAvatarGroup';
2+
3+
export { CollaboratorAvatarGroup };

src/custom/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BBChart } from './BBChart';
33
import { BookmarkNotification } from './BookmarkNotification';
44
import CatalogFilter, { CatalogFilterProps } from './CatalogFilter/CatalogFilter';
55
import { ChapterCard } from './ChapterCard';
6+
import { CollaboratorAvatarGroup } from './CollaboratorAvatarGroup';
67
import { ConnectionChip } from './ConnectionChip';
78
import { CatalogCardDesignLogo, CustomCatalogCard, EmptyStateCard } from './CustomCatalog';
89
import {
@@ -74,6 +75,7 @@ export {
7475
CatalogCardDesignLogo,
7576
CatalogFilter,
7677
ChapterCard,
78+
CollaboratorAvatarGroup,
7779
ConnectionChip,
7880
CustomCatalogCard,
7981
CustomColumnVisibilityControl,

0 commit comments

Comments
 (0)