Skip to content

Commit

Permalink
feat: update avatar component
Browse files Browse the repository at this point in the history
  • Loading branch information
khoilen committed Jan 19, 2025
1 parent 80ff1ac commit 0e18218
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 85 deletions.
1 change: 1 addition & 0 deletions apps/nt-headless-ui/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
name: getAbsolutePath('@storybook/nextjs'),
options: {},
},
staticDirs: ['../public'],
docs: {
autodocs: true,
},
Expand Down
52 changes: 52 additions & 0 deletions apps/nt-headless-ui/components/ui/avatar/avatar.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'

import { Avatar } from './avatar'

describe('Avatar', () => {
it('renders with image source and fallback', () => {
render(<Avatar src="/images/test-avatar.jpg" fallBack="JD" />)
const image = screen.getByRole('img')
const fallback = screen.queryByText('JD')

expect(image).toHaveAttribute(
'src',
'/images/test-avatar.jpg',
)
expect(fallback).not.toBeInTheDocument()
})

it('renders fallback content when image source is not provided', () => {
render(<Avatar fallBack="NA" />)
const fallback = screen.getByText('NA')

expect(fallback).toBeInTheDocument()
})

it('renders a badge with the correct content', () => {
render(<Avatar src="/images/test-avatar.jpg" badge="!" />)
const badge = screen.getByText('!')

expect(badge).toBeInTheDocument()
})

it('positions the badge correctly based on badgePosition prop', () => {
render(
<Avatar
src="/images/test-avatar.jpg"
badge="NEW"
badgePosition="bottom-left"
/>,
)
const badge = screen.getByText('NEW')

expect(badge).toHaveClass('absolute bottom-[3px] left-[1px]')
})

it('does not render a badge when the badge prop is not provided', () => {
render(<Avatar src="/images/test-avatar.jpg" />)
const badge = screen.queryByText('NEW')

expect(badge).not.toBeInTheDocument()
})
})
121 changes: 61 additions & 60 deletions apps/nt-headless-ui/components/ui/avatar/avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,86 @@
import { Meta, StoryObj } from '@storybook/react'
import React from 'react'

import { Avatar, AvatarFallback, AvatarImage } from './avatar'
import { Avatar, AvatarProps } from './avatar'

const URL_IMAGE =
'https://gratisography.com/wp-content/uploads/2024/11/gratisography-augmented-reality-800x525.jpg'
const URL_IMAGE = './image.png'

const meta: Meta = {
const meta: Meta<AvatarProps> = {
title: 'Components/Avatar',
component: Avatar,
subcomponents: { AvatarImage, AvatarFallback },
parameters: {
docs: {
description: {
component:
'A flexible Avatar component using Radix UI.',
},
args: {
src: URL_IMAGE,
fallBack: 'JD',
},
argTypes: {
src: {
control: 'text',
description: 'Source URL for the avatar image.',
},
fallBack: {
control: 'text',
description:
'Fallback content displayed when the image is unavailable.',
},
className: {
control: 'text',
description: 'Custom CSS classes for the avatar root.',
},
badge: {
control: 'text',
description: 'Badge content displayed on the avatar.',
},
badgePosition: {
control: 'select',
options: [
'top-left',
'top-right',
'bottom-left',
'bottom-right',
],
description: 'Position of the badge on the avatar.',
},
},
}

export default meta

type Story = StoryObj<typeof Avatar>

export const Default: Story = {
render: (args) => (
<Avatar {...args}>
<AvatarImage src={URL_IMAGE} alt="User Avatar" />
<AvatarFallback>
<div className="bg-slate-500" />
</AvatarFallback>
</Avatar>
),
export const Default: StoryObj<AvatarProps> = {
args: {
className: '',
},
parameters: {
docs: {
description: {
story: 'Default Avatar with an image and fallback text.',
},
},
src: URL_IMAGE,
fallBack: 'JD',
},
}

export const WithFallbackOnly: Story = {
render: (args) => (
<Avatar {...args}>
<AvatarFallback>
<div className="bg-slate-500" />
</AvatarFallback>
</Avatar>
),
export const NoImage: StoryObj<AvatarProps> = {
args: {
className: '',
src: '',
fallBack: 'NA',
},
parameters: {
docs: {
description: {
story: 'Avatar displaying only fallback text.',
},
},
}

export const CustomSize: StoryObj<AvatarProps> = {
args: {
src: URL_IMAGE,
fallBack: 'CS',
className: 'h-[100px] w-[100px]',
},
}

export const CustomSize: Story = {
render: (args) => (
<Avatar {...args}>
<AvatarImage src={URL_IMAGE} alt="User Avatar" />
<AvatarFallback>loading..</AvatarFallback>
</Avatar>
),
export const NoBadge: StoryObj<AvatarProps> = {
args: {
className: 'h-16 w-16',
src: URL_IMAGE,
fallBack: 'JD',
badge: undefined,
},
parameters: {
docs: {
description: {
story: 'Avatar with a custom size.',
},
},
}

export const CustomBadge: StoryObj<AvatarProps> = {
args: {
src: URL_IMAGE,
fallBack: 'JD',
badge: (
<span className="bg-red-500 text-white px-1 rounded" />
),
badgePosition: 'bottom-right',
},
}
61 changes: 53 additions & 8 deletions apps/nt-headless-ui/components/ui/avatar/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@ import { cn } from '@/lib/utils'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import * as React from 'react'

const Avatar = React.forwardRef<
const AvatarPrimitiveRoot = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
className,
)}
className={cn('relative flex h-20 w-20 shrink-0 ', className)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName

AvatarPrimitiveRoot.displayName = AvatarPrimitive.Root.displayName

const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
className={cn(
'aspect-square h-full w-full rounded-full',
className,
)}
{...props}
/>
))
Expand All @@ -44,4 +45,48 @@ const AvatarFallback = React.forwardRef<
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName

export { Avatar, AvatarImage, AvatarFallback }
export interface AvatarProps
extends React.ComponentPropsWithoutRef<
typeof AvatarPrimitive.Root
> {
src?: string
fallBack?: React.ReactNode
badge?: React.ReactNode
badgePosition?:
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
}

const Avatar = (props: AvatarProps) => {
const { src, fallBack, badge, badgePosition, ...args } = props

const positionClasses = {
'top-left': 'absolute top-[4px] left-[3px]',
'top-right': 'absolute top-[4px] right-[3px]',
'bottom-left': 'absolute bottom-[3px] left-[1px]',
'bottom-right': 'absolute bottom-[3px] right-[1px]',
}

return (
<AvatarPrimitiveRoot {...args}>
<AvatarImage src={src} />
<AvatarFallback>
{fallBack ? fallBack : null}
</AvatarFallback>
{badge && (
<span
className={cn(
'h-4 w-4 rounded-full bg-primary border border-white',
positionClasses[badgePosition!],
)}
>
{badge}
</span>
)}
</AvatarPrimitiveRoot>
)
}

export { Avatar }
2 changes: 0 additions & 2 deletions apps/nt-headless-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@
"test": "pnpm vitest"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint-config-next": "^15.1.4",
"lucide-react": "^0.471.1",
"next": "15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
19 changes: 4 additions & 15 deletions apps/nt-headless-ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added apps/nt-headless-ui/public/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/nt-headless-ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineConfig({
},
},
plugins: [react()],

test: {
globals: true,
environment: 'jsdom',
Expand Down

0 comments on commit 0e18218

Please sign in to comment.