-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Avatar Component #40
Changes from all commits
1c699cf
b2d392d
793445a
df98b8d
10a62f4
16daee5
797883a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,5 +6,5 @@ | |
"semi": true, | ||
"bracketSpacing": true, | ||
"trailingComma": "none", | ||
"endOfLine": "lf" | ||
"endOfLine": "auto" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<div [class]="computedClass()"> | ||
<img [ngSrc]="computedSrc()" [alt]="alt()" [class]="computedImageClass()" (error)="onError()" fill /> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Component, computed, input, signal } from '@angular/core'; | ||
import type { ClassValue } from 'clsx'; | ||
import type { VariantProps } from 'class-variance-authority'; | ||
import { cn } from 'app/utils'; | ||
import { cva } from 'app/storybook.helper'; | ||
import { NgOptimizedImage } from '@angular/common'; | ||
|
||
const [avatarVariants, args, argTypes] = cva('relative flex shrink-0 overflow-hidden rounded-full', { | ||
variants: { | ||
size: { | ||
default: 'h-10 w-10', | ||
sm: 'h-6 w-6 text-xs', | ||
lg: 'h-14 w-14 text-lg' | ||
} | ||
}, | ||
defaultVariants: { | ||
size: 'default' | ||
} | ||
}); | ||
|
||
export { args, argTypes }; | ||
|
||
interface AvatarVariants extends VariantProps<typeof avatarVariants> {} | ||
|
||
@Component({ | ||
selector: 'app-avatar', | ||
standalone: true, | ||
imports: [NgOptimizedImage], | ||
templateUrl: './avatar.component.html' | ||
}) | ||
export class AppAvatarComponent { | ||
class = input<ClassValue>(''); | ||
size = input<AvatarVariants['size']>('default'); | ||
|
||
src = input.required<string>(); | ||
alt = input<string>(''); | ||
imageClass = input<string>(''); | ||
fallback = input<string>('https://placehold.co/56'); | ||
|
||
canShow = signal(true); | ||
|
||
onError = () => { | ||
this.canShow.set(false); | ||
}; | ||
|
||
computedClass = computed(() => cn(avatarVariants({ size: this.size() }), this.class())); | ||
|
||
computedSrc = computed(() => (this.canShow() ? this.src() : this.fallback())); | ||
|
||
computedImageClass = computed(() => cn('aspect-square object-cover h-full w-full', this.imageClass())); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; | ||
import { AppAvatarComponent, args, argTypes } from './avatar.component'; | ||
|
||
const meta: Meta<AppAvatarComponent> = { | ||
title: 'UI/Avatar', | ||
component: AppAvatarComponent, | ||
tags: ['autodocs'], | ||
args: { | ||
...args | ||
}, | ||
argTypes: { | ||
...argTypes | ||
} | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<AppAvatarComponent>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
src: 'https://i.pravatar.cc/40?img=1', | ||
alt: 'avatar', | ||
class: '' | ||
}, | ||
|
||
render: (args) => ({ | ||
props: args, | ||
template: `<app-avatar ${argsToTemplate(args)}></app-avatar>` | ||
}) | ||
}; | ||
|
||
export const Small: Story = { | ||
args: { | ||
size: 'sm', | ||
src: 'https://i.pravatar.cc/24?img=1' | ||
}, | ||
|
||
render: (args) => ({ | ||
props: args, | ||
template: `<app-avatar ${argsToTemplate(args)}></app-avatar>` | ||
}) | ||
}; | ||
|
||
export const Medium: Story = { | ||
args: { | ||
size: 'default', | ||
src: 'https://i.pravatar.cc/40?img=1' | ||
}, | ||
|
||
render: (args) => ({ | ||
props: args, | ||
template: `<app-avatar ${argsToTemplate(args)}></app-avatar>` | ||
}) | ||
}; | ||
|
||
export const Large: Story = { | ||
args: { | ||
size: 'lg', | ||
src: 'https://i.pravatar.cc/56?img=1', | ||
alt: 'avatar', | ||
class: '' | ||
}, | ||
|
||
render: (args) => ({ | ||
props: args, | ||
template: `<app-avatar ${argsToTemplate(args)}></app-avatar>` | ||
}) | ||
}; | ||
|
||
export const WithRandomImage: Story = { | ||
args: { | ||
size: 'lg', | ||
src: 'https://i.pravatar.cc/56', | ||
alt: 'avatar' | ||
}, | ||
|
||
render: (args) => ({ | ||
props: args, | ||
template: `<app-avatar ${argsToTemplate(args)}></app-avatar>` | ||
}) | ||
}; | ||
|
||
export const WithFallback: Story = { | ||
args: { | ||
size: 'default', | ||
src: 'foobar.jpg', | ||
fallback: 'https://placehold.co/40', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Works for me, I might try to do a follow-up that uses content projection just to see how that works. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The disadvantage of the current approach (variables) vs. child-components is of course that it's not possible to use anything other than image URLs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is possible via |
||
alt: 'fallback' | ||
}, | ||
|
||
render: (args) => ({ | ||
props: args, | ||
template: `<app-avatar ${argsToTemplate(args)}></app-avatar>` | ||
}) | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can also define the template shared for all stories somehow if it is the same for all.