Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a UserAvatar component #2898

Merged
merged 3 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/design-system/components/user-avatar/UserAvatar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@use '~scss/utilities' as *;

.#{$prefix}user-avatar {
--#{$prefix}avatar-size-sm: #{get-var('line-height-3xl')};
--#{$prefix}avatar-size-md: #{get-var('line-height-5xl')};
--#{$prefix}avatar-size-lg: #{get-var('line-height-6xl')};

display: flex;
align-items: center;
justify-content: center;
position: relative;

.#{$prefix}avatar {
font-family: get-var('font-family-rubik');
font-weight: get-var('font-weight-semibold');
background-color: get-var('blue-gray-200');
color: get-var('blue-gray-800');

&-sm {
width: var(--#{$prefix}avatar-size-sm);
height: var(--#{$prefix}avatar-size-sm);
font-size: get-var('font-size-md');
}

&-md {
width: var(--#{$prefix}avatar-size-md);
height: var(--#{$prefix}avatar-size-md);
font-size: get-var('font-size-md');
}

&-lg {
width: var(--#{$prefix}avatar-size-lg);
height: var(--#{$prefix}avatar-size-lg);
font-size: get-var('font-size-lg');
}
}

.#{$prefix}user-avatar-status {
position: absolute;
border-radius: 50%;
background-color: get-var('green-500');
right: 0;
bottom: 0;

&-sm {
border: get-var('border-width-md') solid get-var('neutral-0');
width: calc(var(--#{$prefix}avatar-size-sm) * 0.2);
height: calc(var(--#{$prefix}avatar-size-sm) * 0.2);
}

&-md {
border: get-var('border-width-lg') solid get-var('neutral-0');
width: calc(var(--#{$prefix}avatar-size-md) * 0.2);
height: calc(var(--#{$prefix}avatar-size-md) * 0.2);
}

&-lg {
border: get-var('border-width-lg') solid get-var('neutral-0');
width: calc(var(--#{$prefix}avatar-size-lg) * 0.2);
height: calc(var(--#{$prefix}avatar-size-lg) * 0.2);
}
}
}
92 changes: 92 additions & 0 deletions src/design-system/components/user-avatar/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import '~scss-components/user-avatar/UserAvatar.scss'
import { forwardRef } from 'react'
import {
Avatar as MuiAvatar,
AvatarProps as MuiAvatarProps
} from '@mui/material'
import CheckIcon from '@mui/icons-material/Check'
import { cn } from '~/utils/cn'

const variants = ['check', 'avatar', 'monogram', 'photo'] as const
const sizes = ['sm', 'md', 'lg'] as const

interface BaseUserAvatarProps {
variant?: (typeof variants)[number]
src?: string
size?: (typeof sizes)[number]
firstName: string
lastName: string
isOnline?: boolean
}

export type UserAvatarProps = BaseUserAvatarProps &
Omit<MuiAvatarProps, keyof BaseUserAvatarProps>

type Ref = MuiAvatarProps['ref']

const UserAvatar = forwardRef(
(
{
variant = 'avatar',
src,
size = 'sm',
firstName,
lastName,
isOnline,
onClick,
...props
}: UserAvatarProps,
forwardedRef: Ref
) => {
const monogram = firstName.charAt(0) + lastName.charAt(0)

return (
<div className='s2s-user-avatar'>
{variant === 'photo' && src ? (
<MuiAvatar
alt={monogram}
className={cn('s2s-avatar', `s2s-avatar-${size}`)}
onClick={onClick}
ref={forwardedRef}
src={src}
{...props}
/>
) : variant === 'monogram' ? (
<MuiAvatar
onClick={onClick}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can replace cn('s2s-avatar', s2s-avatar-${size}) to variable

ref={forwardedRef}
{...props}
className={cn('s2s-avatar', `s2s-avatar-${size}`)}
>
{monogram}
</MuiAvatar>
) : variant === 'check' ? (
<MuiAvatar
onClick={onClick}
ref={forwardedRef}
{...props}
className={cn('s2s-avatar', `s2s-avatar-${size}`)}
>
<CheckIcon />
</MuiAvatar>
) : (
<MuiAvatar
onClick={onClick}
ref={forwardedRef}
{...props}
className={cn('s2s-avatar', `s2s-avatar-${size}`)}
/>
)}
{isOnline && (
<span
className={`s2s-user-avatar-status s2s-user-avatar-status-${size}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use cn for this className

/>
)}
</div>
)
}
)

UserAvatar.displayName = 'UserAvatar'

export default UserAvatar
168 changes: 168 additions & 0 deletions src/design-system/stories/UserAvatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { Meta, StoryObj } from '@storybook/react'
import { fn } from '@storybook/test'
import UserAvatar from '~scss-components/user-avatar/UserAvatar'

const meta: Meta<typeof UserAvatar> = {
title: 'Components/UserAvatar',
component: UserAvatar,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
The \`UserAvatar\` component is a flexible and customizable avatar element that can be used to display user profile images, initials, and online status. It supports different variants, sizes, and states, making it a versatile component for showcasing user information across your application.

#### Key Features:
- **Variants**: Choose from several types of avatars:
- Photo: Displays a user’s profile image (requires src prop).
- Monogram: Shows the initials of the user based on the firstName and lastName props.
- Check: Displays a check icon inside the avatar.
- Avatar: Displays the user’s initials by default, making it ideal for generic avatars when no photo is available.
- **Sizes:** Adjust the avatar's size to fit your design needs, with options for sm (small), md (medium), or lg (large).
- **Online Status:** Show the user's online status with a small color-coded status dot that appears when isOnline is set to true.
- **Customizable:** Use the sx prop to apply custom styles or override the default styling to match your design requirements.
- **Monogram Generation:** Automatically generates a monogram based on the user’s first and last name, perfect when no image is available.

This component is ideal for user profile representations, whether in a user list, comments section, or anywhere you need to display user information.
`
}
}
},
tags: ['autodocs'],
argTypes: {
variant: {
control: { type: 'select' },
options: ['check', 'avatar', 'monogram', 'photo'],
description: 'The type of avatar to display.',
table: {
type: { summary: 'check | avatar | monogram | photo' }
}
},
size: {
control: { type: 'text' },
description: 'The size of the avatar.',
table: {
type: { summary: 'sm | md | lg' }
}
},
isOnline: {
control: { type: 'boolean' },
description: 'Displays online status if true.',
table: {
type: { summary: 'boolean' }
}
},
src: {
control: { type: 'text' },
description: 'The URL of the image for the photo variant.',
table: {
type: { summary: 'string' }
}
},
firstName: {
control: { type: 'text' },
description: 'The first name of the user.',
table: {
type: { summary: 'string' }
}
},
lastName: {
control: { type: 'text' },
description: 'The last name of the user.',
table: {
type: { summary: 'string' }
}
}
},
args: {
variant: 'avatar',
size: 'sm',
firstName: 'Test',
lastName: 'User',
isOnline: false,
onClick: fn()
}
}

export default meta
type Story = StoryObj<typeof meta>

export const All: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '30px' }}>
<UserAvatar
{...args}
src='/src/assets/img/user-profile-page/avatar.png'
variant='photo'
/>
<UserAvatar {...args} variant='monogram' />
<UserAvatar {...args} variant='check' />
<UserAvatar {...args} variant='avatar' />
</div>
),
parameters: {
docs: {
description: {
story:
'This story showcases all avatar variants in a single row for easy comparison.'
}
}
}
}

export const PhotoVariant: Story = {
parameters: {
docs: {
description: {
story:
'Displays the photo variant of the UserAvatar component. Use this variant when displaying a user profile picture.'
}
}
},
args: {
variant: 'photo',
src: '/src/assets/img/user-profile-page/avatar.png'
}
}

export const MonogramVariant: Story = {
parameters: {
docs: {
description: {
story:
'Displays the monogram variant of the UserAvatar component. The user’s initials are shown as a fallback when no photo is provided.'
}
}
},
args: {
variant: 'monogram'
}
}

export const CheckVariant: Story = {
parameters: {
docs: {
description: {
story:
'Displays the check variant of the UserAvatar component. This variant includes a check icon, typically used for selection or confirmation purposes.'
}
}
},
args: {
variant: 'check'
}
}

export const AvatarVariant: Story = {
parameters: {
docs: {
description: {
story:
'Displays the default avatar variant of the UserAvatar component. The initials of the user are displayed by default, with optional online status.'
}
}
},
args: {
variant: 'avatar'
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please fix Sonar Issue and add more coverage on tests

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render, screen } from '@testing-library/react';
import UserAvatar from '~scss-components/user-avatar/UserAvatar';

const firstName = 'John';
const lastName = 'Doe';
const avatarSrc = '/src/assets/img/user-profile-page/avatar.png';
const isOnline = true;

describe('UserAvatar Component', () => {
it('should render with monogram variant', () => {
render(
<UserAvatar
variant="monogram"
firstName={firstName}
lastName={lastName}
isOnline={isOnline}
/>
);

const monogramElement = screen.getByText(firstName.charAt(0) + lastName.charAt(0));
expect(monogramElement).toBeInTheDocument();
});

it('should show online status indicator when isOnline is true', () => {
render(
<UserAvatar
variant="photo"
firstName={firstName}
lastName={lastName}
isOnline={true}
src={avatarSrc}
/>
);

const onlineStatus = document.querySelector('.s2s-user-avatar-status');
expect(onlineStatus).toBeInTheDocument();
});

it('should not show online status indicator when isOnline is false', () => {
render(
<UserAvatar
variant="photo"
firstName={firstName}
lastName={lastName}
isOnline={false}
src={avatarSrc}
/>
);

const onlineStatus = document.querySelector('.s2s-user-avatar-status');
expect(onlineStatus).not.toBeInTheDocument();
});

});