Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
60 changes: 8 additions & 52 deletions src/components/atoms/avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Icon from '@atoms/icon';
import type { Meta, StoryObj } from '@storybook/react';
import Avatar from './Avatar';

Expand Down Expand Up @@ -70,61 +69,18 @@ export const WithImage: Story = {
};

/**
* - You can add a badge to the avatar by using the `hasBadge` prop.
* - The `badgeContent` prop allows you to customize the badge text.
* - You can round the avatar using the `rounded` prop.
* - The available options are 'md', 'full', and 'none'.
* - The default value is 'md'.
* - This prop allows for customization of the avatar's appearance.
*/

export const WithBadge: Story = {
export const Rounded: Story = {
render: () => (
<div className='flex gap-4 items-center'>
<Avatar
src='/images/logo-dark-background.png'
alt='EG'
size='sm'
hasBadge={true}
badgeContent='1'
badgeClassName='bg-red-500 text-white'
/>
<Avatar
src='/images/logo-dark-background.png'
alt='EG'
size='md'
hasBadge={true}
badgeContent='2'
badgeClassName='bg-blue-500 text-white'
/>
<Avatar
src='/images/logo-dark-background.png'
alt='EG'
size='lg'
hasBadge={true}
badgeContent='3'
badgeClassName='bg-green-500 text-white'
/>
<Avatar
src='/images/logo-dark-background.png'
alt='EG'
size='sm'
hasBadge={true}
badgeContent={<Icon name='eye' size={14} color='text-text-dark' colorDark='dark:text-text-dark' />}
badgeClassName='bg-red-500 text-white'
/>
<Avatar
src='/images/logo-dark-background.png'
alt='EG'
size='md'
hasBadge={true}
badgeContent={<Icon name='calendar-clock' size={14} color='text-text-dark' colorDark='dark:text-text-dark' />}
badgeClassName='bg-blue-500 text-white'
/>
<Avatar
src='/images/logo-dark-background.png'
alt='EG'
size='lg'
hasBadge={true}
badgeContent={<Icon name='check' size={14} color='text-text-dark' colorDark='dark:text-text-dark' />}
badgeClassName='bg-green-500 text-white'
/>
<Avatar src='' alt='EG' size='md' rounded='md' />
<Avatar src='' alt='EG' size='md' rounded='full' />
<Avatar src='' alt='EG' size='md' rounded='none' />
</div>
)
};
72 changes: 18 additions & 54 deletions src/components/atoms/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { cn } from '@/lib/utils';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import type { ComponentProps } from 'react';
import type { ComponentProps, FC } from 'react';
import type { AvatarProps } from './types';
import { useAvatar } from './useAvatar';

function AvatarContainer({ className, ...props }: ComponentProps<typeof AvatarPrimitive.Root>) {
return (
Expand Down Expand Up @@ -29,62 +30,25 @@ function AvatarFallback({ className, ...props }: ComponentProps<typeof AvatarPri
);
}

const Avatar = ({
src,
alt = 'EG',
className,
size = 'md',
hasBadge = false,
badgeContent,
badgeClassName
}: AvatarProps) => {
const sizeClasses = {
sm: '30px',
md: '40px',
lg: '50px',
xl: '60px',
'2xl': '70px',
'3xl': '80px'
};
const textClasses = {
sm: 'text-[0.8em]',
md: 'text-[1em]',
lg: 'text-[1.2em]',
xl: 'text-[1.4em]',
'2xl': 'text-[1.6em]',
'3xl': 'text-[1.8em]'
};
const sizeClass = sizeClasses[size];
const textClass = textClasses[size];
const Avatar: FC<AvatarProps> = ({ ...props }) => {
const { src, alt, sizeClass, className, textClass, roundedClass } = useAvatar({ ...props });

return (
<div className='relative inline-block'>
<AvatarContainer
className={cn(
'dark:bg-gray-700 rounded-full flex items-center justify-center shadow-sm shadow-gray-light-800 dark:shadow-gray-dark-800',
className
)}
style={{ width: sizeClass, height: sizeClass }}
role="img"
aria-label={alt}
>
<AvatarImage src={src} style={{ width: sizeClass, height: sizeClass }} />
<AvatarFallback className={cn('text-text-light dark:text-text-dark leading-[1.2] pt-[0.2em]', textClass)}>
{alt}
</AvatarFallback>
</AvatarContainer>
{hasBadge && (
<span
className={cn(
'absolute -top-2 -right-2 flex items-center justify-center rounded-full bg-accent text-white text-xs',
badgeClassName
)}
style={{ width: '20px', height: '20px' }}
>
{badgeContent}
</span>
<AvatarContainer
className={cn(
'dark:bg-gray-700 flex items-center justify-center shadow-sm shadow-gray-light-800 dark:shadow-gray-dark-800',
roundedClass,
className
)}
</div>
style={{ width: sizeClass, height: sizeClass }}
role='img'
aria-label={alt}
>
<AvatarImage src={src} style={{ width: sizeClass, height: sizeClass }} />
<AvatarFallback className={cn('text-text-light dark:text-text-dark leading-[1.2] pt-[0.2em]', textClass)}>
{alt}
</AvatarFallback>
</AvatarContainer>
);
};

Expand Down
13 changes: 3 additions & 10 deletions src/components/atoms/avatar/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
type ThemeRounded = 'md' | 'full' | 'none';

export type AvatarProps = {
/** @control src */
Expand All @@ -9,13 +9,6 @@ export type AvatarProps = {
alt: string;
/** @control text */
className?: string;
/**
* @control boolean
* @default false
*/
hasBadge?: boolean;
/** @control text */
badgeContent?: string | ReactNode;
/** @control text */
badgeClassName?: string;
/** @control select */
rounded?: ThemeRounded;
};
33 changes: 33 additions & 0 deletions src/components/atoms/avatar/useAvatar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { AvatarProps } from './types';

export const useAvatar = ({ src, alt = 'EG', className, size = 'md', rounded = 'md' }: AvatarProps) => {
const sizeClasses = {
sm: '30px',
md: '40px',
lg: '50px',
xl: '60px',
'2xl': '70px',
'3xl': '80px'
};
const textClasses = {
sm: 'text-[0.8em]',
md: 'text-[1em]',
lg: 'text-[1.2em]',
xl: 'text-[1.4em]',
'2xl': 'text-[1.6em]',
'3xl': 'text-[1.8em]'
};

const roundedClass = rounded ? `rounded-${rounded}` : '';
const sizeClass = sizeClasses[size];
const textClass = textClasses[size];

return {
src,
alt,
className,
sizeClass,
textClass,
roundedClass
};
};
44 changes: 20 additions & 24 deletions src/components/atoms/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { cn } from '@/lib/utils';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { ChevronRightIcon } from 'lucide-react';
import type { FC } from 'react';
import { SpinnerCircular } from 'spinners-react';
import type { DropdownElement, DropdownProps } from './types';
import { useDropdown } from './useDropdown';

const renderDropdownItem = (element: DropdownElement, index: number) => {
if (element.type === 'item') {
Expand All @@ -15,6 +17,7 @@ const renderDropdownItem = (element: DropdownElement, index: number) => {
className={cn(
'relative flex cursor-default justify-between items-center gap-2 rounded-sm px-2 py-1.5 text-sm',
'transition-opacity duration-300 ease-in-out',
'hover:outline-offset-1 dark:hover:outline-white hover:outline-secondary hover:outline-1',
'focus-visible:outline-offset-1 dark:focus-visible:outline-white focus-visible:outline-secondary focus-visible:outline-1',
element.variant === 'destructive' && 'bg-secondary text-text-dark hover:bg-red-secondary-hover'
)}
Expand All @@ -40,6 +43,7 @@ const renderDropdownSubmenu = (element: DropdownElement, index: number) => {
className={cn(
'flex items-center rounded-md justify-between px-2 py-1.5 text-sm',
'transition-opacity duration-300 ease-in-out',
'hover:outline-offset-1 dark:hover:outline-white hover:outline-secondary hover:outline-1',
'focus-visible:outline-offset-1 dark:focus-visible:outline-white focus-visible:outline-secondary focus-visible:outline-1'
)}
>
Expand Down Expand Up @@ -92,34 +96,26 @@ const renderDropdownElement = (element: DropdownElement, index: number) => {
}
};

const Dropdown = ({
width = '56px',
position = 'bottom',
align = 'center',
offset = 1,
closeOnSelect = true,
items,
loading = false,
children,
className
}: DropdownProps) => {
const marginClasses = {
top: 'mb-2',
bottom: 'mt-2',
left: 'mr-2',
right: 'ml-2'
};

const firstLabelId = items.find((item) => item.type === 'label')?.label
? `dropdown-label-${items.findIndex((item) => item.type === 'label')}`
: undefined;
const fallbackId = 'dropdown-fallback-label';
const accessibleLabelId = firstLabelId || fallbackId;
const Dropdown: FC<DropdownProps> = ({ ...props }) => {
const {
items,
loading,
closeOnSelect,
position,
align,
offset,
width,
className,
accessibleLabelId,
marginClasses,
firstLabelId,
fallbackId
} = useDropdown(props);

return (
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild={true}>
<div>{children}</div>
<div>{props.children}</div>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
Expand Down
40 changes: 40 additions & 0 deletions src/components/atoms/dropdown/useDropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { DropdownProps } from './types';

export const useDropdown = ({
items = [],
loading = false,
closeOnSelect = true,
position = 'bottom',
align = 'start',
offset = 0,
width = 'auto',
className = ''
}: DropdownProps) => {
const marginClasses = {
top: 'mb-2',
bottom: 'mt-2',
left: 'mr-2',
right: 'ml-2'
};

const firstLabelId = items.find((item) => item.type === 'label')?.label
? `dropdown-label-${items.findIndex((item) => item.type === 'label')}`
: undefined;
const fallbackId = 'dropdown-fallback-label';
const accessibleLabelId = firstLabelId || fallbackId;

return {
items,
loading,
closeOnSelect,
position,
align,
offset,
width,
className,
accessibleLabelId,
marginClasses,
firstLabelId,
fallbackId
};
};
2 changes: 1 addition & 1 deletion src/components/atoms/icon-button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type VariantProps, cva } from 'class-variance-authority';

export const iconButtonVariants = cva(
[
'link relative overflow-hidden border-2 cursor-pointer px-2 py-2 max-w-full',
'link relative overflow-hidden border-2 cursor-pointer px-1 py-1 max-w-full',
'transition-all duration-200 ease-in-out',
'flex items-center justify-start',
'whitespace-nowrap line-clamp-1 ',
Expand Down
Loading