mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
Show a user's avatar on the public profile (#333)
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { expect, within } from 'storybook/test';
|
||||
import ProfileAvatar from './ProfileAvatar';
|
||||
|
||||
const profileAvatarDescription = `Avatar shown at the top of the public profile sidebar. It renders the photo from the public profile payload's \`avatarUrl\`, and shows the first two letters of the display name when that field is empty or the image fails to load.`;
|
||||
|
||||
const meta = {
|
||||
title: 'Users/ProfileAvatar',
|
||||
component: ProfileAvatar,
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
displayName: 'Maren Iverson',
|
||||
avatarUrl: 'https://picsum.photos/id/1027/240/240',
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: profileAvatarDescription,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ProfileAvatar>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const WithPhoto: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const image = canvas.getByRole('img', { name: "Maren Iverson's avatar" });
|
||||
await expect(image).toHaveAttribute('src', 'https://picsum.photos/id/1027/240/240');
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutPhoto: Story = {
|
||||
args: { avatarUrl: '' },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(canvas.getByText('MA')).toBeInTheDocument();
|
||||
await expect(canvas.queryByRole('img')).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const BrokenPhotoUrl: Story = {
|
||||
args: { avatarUrl: 'https://example.invalid/missing-avatar.jpg' },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(await canvas.findByText('MA')).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ProfileAvatarProps {
|
||||
avatarUrl: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user's current avatar photo, falling back to their initials when the public
|
||||
* profile payload carries an empty `avatarUrl` or the hosted image fails to load.
|
||||
*/
|
||||
const ProfileAvatar = ({ avatarUrl, displayName }: ProfileAvatarProps) => {
|
||||
const [didImageFail, setDidImageFail] = useState(false);
|
||||
const showPhoto = avatarUrl.length > 0 && !didImageFail;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`avatar w-[120px] h-[120px] rounded-full border-4 border-base-200 shadow-md ${
|
||||
showPhoto ? '' : 'avatar-placeholder'
|
||||
}`}
|
||||
>
|
||||
{showPhoto ? (
|
||||
<div className="w-full h-full rounded-full overflow-hidden">
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={`${displayName}'s avatar`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={() => setDidImageFail(true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-secondary text-secondary-content w-full h-full rounded-full flex items-center justify-center">
|
||||
<span className="text-2xl font-bold">
|
||||
{displayName.slice(0, 2).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileAvatar;
|
||||
Reference in New Issue
Block a user