Skip to content

Commit

Permalink
Create a UserAvatar component (#2898)
Browse files Browse the repository at this point in the history
* Create a UserAvatar component, style it in scss, add it to stirybook, write tests for it

* added tests

* fixed sonar issues, made changes according to comments to PR
  • Loading branch information
PavloButynets authored Dec 4, 2024
1 parent b2b6410 commit bd0a2e1
Show file tree
Hide file tree
Showing 4 changed files with 409 additions and 0 deletions.
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);
}
}
}
106 changes: 106 additions & 0 deletions src/design-system/components/user-avatar/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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)

const avatarClass = cn('s2s-avatar', `s2s-avatar-${size}`)

let avatarContent
if (variant === 'photo' && src) {
avatarContent = (
<MuiAvatar
alt={monogram}
className={avatarClass}
onClick={onClick}
ref={forwardedRef}
src={src}
{...props}
/>
)
} else if (variant === 'monogram') {
avatarContent = (
<MuiAvatar
onClick={onClick}
ref={forwardedRef}
{...props}
className={avatarClass}
>
{monogram}
</MuiAvatar>
)
} else if (variant === 'check') {
avatarContent = (
<MuiAvatar
onClick={onClick}
ref={forwardedRef}
{...props}
className={avatarClass}
>
<CheckIcon />
</MuiAvatar>
)
} else {
avatarContent = (
<MuiAvatar
onClick={onClick}
ref={forwardedRef}
{...props}
className={avatarClass}
/>
)
}

return (
<div className='s2s-user-avatar'>
{avatarContent}
{isOnline && (
<span
className={cn(
's2s-user-avatar-status',
`s2s-user-avatar-status-${size}`
)}
/>
)}
</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'
}
}
Loading

0 comments on commit bd0a2e1

Please sign in to comment.