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
51 changes: 51 additions & 0 deletions frontend/src/components/Avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from '@storybook/react';

import Avatar from '.';

const meta: Meta = {
title: 'Components/Avatar',
component: Avatar,
parameters: {
backgrounds: {
default: 'dark',
},
},
argTypes: {
size: {
control: { type: 'radio', options: ['sm', 'lg'] },
},
imageUrls: {
control: false,
},
},
} satisfies Meta<typeof Avatar>;

export default meta;

type Story = StoryObj<typeof Avatar>;

export const Default: Story = {
args: {
size: 'sm',
imageUrls: [
'https://picsum.photos/200',
'https://picsum.photos/201',
'https://picsum.photos/202',
'https://picsum.photos/203',
'https://picsum.photos/204',
'https://picsum.photos/205',
],
},
};

export const FetchError: Story = {
args: {
size: 'sm',
imageUrls: [
'https://hi.com/',
'https://hi.com/',
'https://hi.com/',
'https://hi.com/',
],
},
};
27 changes: 27 additions & 0 deletions frontend/src/components/Avatar/AvatarCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { vars } from '@/theme/index.css';

import { Flex } from '../Flex';
import type { Typo } from '../Text';
import { Text } from '../Text';
import type { Size } from '.';
import { avatarItemStyle } from './index.css';

interface AvatarCountProps {
size: Size;
count: number;
}

const AvatarCount = ({ size, count }: AvatarCountProps) => (
<Flex
align='center'
className={avatarItemStyle({ size })}
justify='center'
>
<Text color={vars.color.Ref.Netural[500]} typo={getTypo(size)}>{count}</Text>
</Flex>
);

const getTypo = (size: Size): Typo =>
size === 'sm' ? 'caption' : 't3';

export default AvatarCount;
34 changes: 34 additions & 0 deletions frontend/src/components/Avatar/AvatarItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useState } from 'react';

import type { Size } from '.';
import { avatarItemStyle } from './index.css';

interface AvatarItemProps {
src: string;
size?: Size;
alt?: string;
}

const AvatarItem = ({ src, size = 'sm', alt }: AvatarItemProps) => {
const [imgSrc, setImgSrc] = useState(src);
const [hasError, setHasError] = useState(false);
const fallbackSrc = 'https://picsum.photos/id/200/200/200';
const handleError = () => {
if (!hasError) {
setImgSrc(fallbackSrc);
setHasError(true);
}
};

return (
<img
alt={alt || `Avatar image ${imgSrc}`}
className={avatarItemStyle({ size })}
loading='lazy'
onError={handleError}
src={imgSrc}
/>
);
};

export default AvatarItem;
9 changes: 7 additions & 2 deletions frontend/src/components/Avatar/index.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ export const avatarItemStyle = recipe({
base: {
backgroundColor: vars.color.Ref.Netural['White'],
borderRadius: vars.radius['Max'],
border: `2px solid ${vars.color.Ref.Netural[500]}`,
border: `2px solid ${vars.color.Ref.Netural['White']}`,
selectors: {
'&:last-child': {
borderColor: vars.color.Ref.Netural[100],
},
},
},
variants: {
size: {
Expand Down Expand Up @@ -45,4 +50,4 @@ export const avatarCountStyle = recipe({
color: vars.color.Ref.Netural[500],
border: `2px solid ${vars.color.Ref.Netural[100]}`,
},
});
});
37 changes: 37 additions & 0 deletions frontend/src/components/Avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import AvatarCount from './AvatarCount';
import AvatarItem from './AvatarItem';
import { avatarContainerStyle } from './index.css';

export type Size = 'sm' | 'lg';

interface AvatarProps {
size: Size;
imageUrls: string[];
}
Comment on lines +7 to +10
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding prop validation for imageUrls.

Add validation to ensure imageUrls is not empty and contains valid URLs.

 interface AvatarProps {
   size: Size;
-  imageUrls: string[];
+  imageUrls: string[] & { length: number };
 }

Committable suggestion skipped: line range outside the PR's diff.


const MAX_IMAGE_COUNT = 4;

const Avatar = ({ size, imageUrls }: AvatarProps) => {
const ENTIRE_LENGTH = imageUrls.length;
const limitedUrls = imageUrls.slice(0, MAX_IMAGE_COUNT);

return (
<div className={avatarContainerStyle}>
{limitedUrls.map((url, index) => (
<AvatarItem
key={`${index}-${url}`}
size={size}
src={url}
/>
))}
{ENTIRE_LENGTH > MAX_IMAGE_COUNT && (
<AvatarCount
count={ENTIRE_LENGTH}
size={size}
/>
)}
</div>
);
};

export default Avatar;