From 9f5f704566b02134470024783e5e30239c07a646 Mon Sep 17 00:00:00 2001 From: Arnab Date: Thu, 15 Jan 2026 14:59:14 +0530 Subject: [PATCH 1/4] feat(avatar): Introduce Avatar component with image and fallback support This commit introduces a new atomic `Avatar` component, designed to display user avatars. Key features include: - **Image display:** Renders an `` element when a `src` prop is provided. - **Fallback mechanism:** Displays initials or a single character as a fallback when `src` is not available or fails to load. - **Multiple sizes:** Supports predefined sizes (`xs`, `sm`, `md`, `lg`, `xl`) for consistent scaling. - **Accessibility:** Includes `alt` text for images and `aria-label` for fallback elements. - **Storybook integration:** Provides comprehensive stories demonstrating various use cases, including default, image, broken image, and size variations. This component enhances the UI library by providing a flexible and robust solution for displaying user profiles. --- .../atoms/Avatar/Avatar.stories.tsx | 61 +++++++++++++ src/components/atoms/Avatar/Avatar.tsx | 87 +++++++++++++++++++ src/components/atoms/Avatar/AvatarProps.ts | 9 ++ 3 files changed, 157 insertions(+) create mode 100644 src/components/atoms/Avatar/Avatar.stories.tsx create mode 100644 src/components/atoms/Avatar/Avatar.tsx create mode 100644 src/components/atoms/Avatar/AvatarProps.ts diff --git a/src/components/atoms/Avatar/Avatar.stories.tsx b/src/components/atoms/Avatar/Avatar.stories.tsx new file mode 100644 index 0000000..8c28dd0 --- /dev/null +++ b/src/components/atoms/Avatar/Avatar.stories.tsx @@ -0,0 +1,61 @@ +import type {Meta, StoryObj} from "@storybook/react-vite"; +import {Avatar} from "./Avatar"; + +const meta = { + title: "components/atoms/Avatar", + component: Avatar, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * Default avatar with initials fallback + */ +export const Default: Story = { + args: { + fallback: "AM", + alt: "Arnab Mandal", + size: "md", + }, +}; + +/** + * Avatar with real image + */ +export const WithImage: Story = { + args: { + src: "https://i.pravatar.cc/150?img=5", + fallback: "AM", + alt: "User profile", + size: "md", + }, +}; + +/** + * Broken image → fallback test + */ +export const BrokenImage: Story = { + args: { + src: "https://example.com/invalid-image.jpg", + fallback: "NA", + alt: "Broken avatar", + size: "md", + }, +}; + +/** + * All sizes preview + */ +export const Sizes: Story = { + render: () => ( +
+ + + + + +
+ ), +}; diff --git a/src/components/atoms/Avatar/Avatar.tsx b/src/components/atoms/Avatar/Avatar.tsx new file mode 100644 index 0000000..148f200 --- /dev/null +++ b/src/components/atoms/Avatar/Avatar.tsx @@ -0,0 +1,87 @@ +import type {AvatarProps, AvatarSize} from "./AvatarProps"; + +const sizeMap: Record = { + xs: 24, + sm: 32, + md: 40, + lg: 56, + xl: 72, +}; + +/** + * Renders an Avatar component. + * + * This component displays a user's avatar, either from an image URL or a text fallback. + * It supports various predefined sizes and custom styling via `className`. + * + * @param {Readonly} props - The properties for the Avatar component. + * @returns {JSX.Element} The rendered Avatar component. + * + * @example + * // Basic usage with an image + * + * + * @example + * // Avatar with a specific size and fallback text + * + * + * @example + * // Avatar with custom class + * + * + * @example + * // Avatar with no src and no fallback (will show '?') + * + */ + +export function Avatar(props: Readonly) { + const { + src, + alt = "Avatar", + size = "md", + fallback, + className, + } = props; + + const pixelSize = sizeMap[size]; + + // IMAGE RENDER + if (src) { + return ( + {alt} + ); + } + + // FALLBACK RENDER + return ( +
+ {fallback?.slice(0, 2).toUpperCase() ?? "?"} +
+ ); +} diff --git a/src/components/atoms/Avatar/AvatarProps.ts b/src/components/atoms/Avatar/AvatarProps.ts new file mode 100644 index 0000000..ffccec9 --- /dev/null +++ b/src/components/atoms/Avatar/AvatarProps.ts @@ -0,0 +1,9 @@ +export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl"; + +export interface AvatarProps { + src?: string; + alt?: string; + size?: AvatarSize; + fallback?: string; // initials OR single char OR icon text + className?: string; +} From c95b630399a4369f8ecc03951bf4cedc062edd81 Mon Sep 17 00:00:00 2001 From: Arnab Date: Thu, 15 Jan 2026 15:25:01 +0530 Subject: [PATCH 2/4] feat(Avatar): add image error handling and fallback logic ## Description Updated the `Avatar` component to gracefully handle image loading failures. Previously, a broken URL would result in a broken image icon. The component now tracks the image loading state and automatically renders the fallback content if the image fails to load. Additionally, removed verbose JSDoc comments to simplify the file. ## Type of Change - [x] New feature - [ ] Bug fix - [x] Refactoring - [ ] Documentation update - [ ] Test addition/update ## Changes Made - Imported `useState` to manage image error state. - Added `onError` event handler to the `` element to detect loading failures. - Updated rendering logic to display the image only if `src` is present and no error has occurred. - Removed the JSDoc block for the `Avatar` function. - Removed inline comment regarding background color. ## Testing Done - [x] All tests pass - [ ] Added new tests - [x] Manual testing completed ## Screenshots (if applicable) N/A ## Related Issues N/A ## Checklist - [x] Code follows project style guidelines - [x] Self-review completed - [x] No console.log or debugging code in production logic - [x] All tests passing --- src/components/atoms/Avatar/Avatar.tsx | 38 ++++++-------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/src/components/atoms/Avatar/Avatar.tsx b/src/components/atoms/Avatar/Avatar.tsx index 148f200..8514ff8 100644 --- a/src/components/atoms/Avatar/Avatar.tsx +++ b/src/components/atoms/Avatar/Avatar.tsx @@ -1,3 +1,4 @@ +import {useState} from "react"; import type {AvatarProps, AvatarSize} from "./AvatarProps"; const sizeMap: Record = { @@ -8,32 +9,6 @@ const sizeMap: Record = { xl: 72, }; -/** - * Renders an Avatar component. - * - * This component displays a user's avatar, either from an image URL or a text fallback. - * It supports various predefined sizes and custom styling via `className`. - * - * @param {Readonly} props - The properties for the Avatar component. - * @returns {JSX.Element} The rendered Avatar component. - * - * @example - * // Basic usage with an image - * - * - * @example - * // Avatar with a specific size and fallback text - * - * - * @example - * // Avatar with custom class - * - * - * @example - * // Avatar with no src and no fallback (will show '?') - * - */ - export function Avatar(props: Readonly) { const { src, @@ -43,10 +18,14 @@ export function Avatar(props: Readonly) { className, } = props; + const [hasError, setHasError] = useState(false); + const pixelSize = sizeMap[size]; - // IMAGE RENDER - if (src) { + const showImage = src && !hasError; + + // IMAGE RENDER (only if no error) + if (showImage) { return ( ) { width={pixelSize} height={pixelSize} className={className} + onError={() => setHasError(true)} style={{ borderRadius: "50%", objectFit: "cover", @@ -74,7 +54,7 @@ export function Avatar(props: Readonly) { display: "flex", alignItems: "center", justifyContent: "center", - backgroundColor: "#e5e7eb", // neutral gray + backgroundColor: "#e5e7eb", color: "#374151", fontWeight: 600, fontSize: Math.floor(pixelSize / 2.5), From 1ac4e609be0530787ad00e796882fcf01b8a911c Mon Sep 17 00:00:00 2001 From: Arnab Date: Thu, 15 Jan 2026 15:34:29 +0530 Subject: [PATCH 3/4] docs(Avatar): Add documentation add js doc --- src/components/atoms/Avatar/Avatar.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/components/atoms/Avatar/Avatar.tsx b/src/components/atoms/Avatar/Avatar.tsx index 8514ff8..34eaa93 100644 --- a/src/components/atoms/Avatar/Avatar.tsx +++ b/src/components/atoms/Avatar/Avatar.tsx @@ -9,6 +9,32 @@ const sizeMap: Record = { xl: 72, }; +/** + * Renders an Avatar component. + * + * This component displays a user's avatar, either from an image URL or a text fallback. + * It supports various predefined sizes and custom styling via `className`. + * + * @param {Readonly} props - The properties for the Avatar component. + * @returns {JSX.Element} The rendered Avatar component. + * + * @example + * // Basic usage with an image + * + * + * @example + * // Avatar with a specific size and fallback text + * + * + * @example + * // Avatar with custom class + * + * + * @example + * // Avatar with no src and no fallback (will show '?') + * + */ + export function Avatar(props: Readonly) { const { src, From 45437d650ac77ceadab22bf43f577682f1e5df84 Mon Sep 17 00:00:00 2001 From: Arnab Date: Thu, 15 Jan 2026 15:41:18 +0530 Subject: [PATCH 4/4] fix(Avatar): improve image error handling and state management ## Description Updated the `Avatar` component to correctly reset its error state when the `src` prop changes, ensuring that new images are attempted even after a previous failure. Enhanced the `onError` handler to prevent infinite loops and refined the JSDoc documentation for better readability. ## Type of Change - [ ] New feature - [x] Bug fix - [x] Refactoring - [x] Documentation update - [ ] Test addition/update ## Changes Made - Added `useEffect` to reset `hasError` to `false` when `src` changes - Updated `onError` to set `e.currentTarget.onerror = null` to prevent infinite loops - Changed `src` check to `Boolean(src)` for explicit type coercion - Simplified and clarified JSDoc comments and examples ## Testing Done - [x] All tests pass - [ ] Added new tests - [x] Manual testing completed --- src/components/atoms/Avatar/Avatar.tsx | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/atoms/Avatar/Avatar.tsx b/src/components/atoms/Avatar/Avatar.tsx index 34eaa93..44cfa46 100644 --- a/src/components/atoms/Avatar/Avatar.tsx +++ b/src/components/atoms/Avatar/Avatar.tsx @@ -1,4 +1,4 @@ -import {useState} from "react"; +import {useEffect, useState} from "react"; import type {AvatarProps, AvatarSize} from "./AvatarProps"; const sizeMap: Record = { @@ -12,29 +12,24 @@ const sizeMap: Record = { /** * Renders an Avatar component. * - * This component displays a user's avatar, either from an image URL or a text fallback. - * It supports various predefined sizes and custom styling via `className`. + * Displays a user profile image when `src` is provided. + * Falls back to initials or a placeholder when the image is missing or fails to load. * - * @param {Readonly} props - The properties for the Avatar component. - * @returns {JSX.Element} The rendered Avatar component. + * @param {Readonly} props - Avatar properties + * @returns {React.ReactNode} Rendered avatar * * @example - * // Basic usage with an image * * * @example - * // Avatar with a specific size and fallback text * * * @example - * // Avatar with custom class - * + * * * @example - * // Avatar with no src and no fallback (will show '?') * */ - export function Avatar(props: Readonly) { const { src, @@ -46,11 +41,15 @@ export function Avatar(props: Readonly) { const [hasError, setHasError] = useState(false); - const pixelSize = sizeMap[size]; + // Reset error state when image source changes + useEffect(() => { + setHasError(false); + }, [src]); - const showImage = src && !hasError; + const pixelSize = sizeMap[size]; + const showImage = Boolean(src) && !hasError; - // IMAGE RENDER (only if no error) + // IMAGE RENDER if (showImage) { return ( ) { width={pixelSize} height={pixelSize} className={className} - onError={() => setHasError(true)} + onError={(e) => { + // prevent infinite error loop + e.currentTarget.onerror = null; + setHasError(true); + }} style={{ borderRadius: "50%", objectFit: "cover",