diff --git a/webapp/.prettierrc b/webapp/.prettierrc index 3cc88595..c188b0b1 100644 --- a/webapp/.prettierrc +++ b/webapp/.prettierrc @@ -6,5 +6,5 @@ "semi": true, "bracketSpacing": true, "trailingComma": "none", - "endOfLine": "lf" + "endOfLine": "auto" } diff --git a/webapp/src/app/ui/avatar/avatar.component.html b/webapp/src/app/ui/avatar/avatar.component.html new file mode 100644 index 00000000..d57806aa --- /dev/null +++ b/webapp/src/app/ui/avatar/avatar.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/webapp/src/app/ui/avatar/avatar.component.ts b/webapp/src/app/ui/avatar/avatar.component.ts new file mode 100644 index 00000000..d2eb5a09 --- /dev/null +++ b/webapp/src/app/ui/avatar/avatar.component.ts @@ -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 {} + +@Component({ + selector: 'app-avatar', + standalone: true, + imports: [NgOptimizedImage], + templateUrl: './avatar.component.html' +}) +export class AppAvatarComponent { + class = input(''); + size = input('default'); + + src = input.required(); + alt = input(''); + imageClass = input(''); + fallback = input('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())); +} diff --git a/webapp/src/app/ui/avatar/avatar.stories.ts b/webapp/src/app/ui/avatar/avatar.stories.ts new file mode 100644 index 00000000..2aadf187 --- /dev/null +++ b/webapp/src/app/ui/avatar/avatar.stories.ts @@ -0,0 +1,95 @@ +import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; +import { AppAvatarComponent, args, argTypes } from './avatar.component'; + +const meta: Meta = { + title: 'UI/Avatar', + component: AppAvatarComponent, + tags: ['autodocs'], + args: { + ...args + }, + argTypes: { + ...argTypes + } +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + src: 'https://i.pravatar.cc/40?img=1', + alt: 'avatar', + class: '' + }, + + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const Small: Story = { + args: { + size: 'sm', + src: 'https://i.pravatar.cc/24?img=1' + }, + + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const Medium: Story = { + args: { + size: 'default', + src: 'https://i.pravatar.cc/40?img=1' + }, + + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const Large: Story = { + args: { + size: 'lg', + src: 'https://i.pravatar.cc/56?img=1', + alt: 'avatar', + class: '' + }, + + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const WithRandomImage: Story = { + args: { + size: 'lg', + src: 'https://i.pravatar.cc/56', + alt: 'avatar' + }, + + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const WithFallback: Story = { + args: { + size: 'default', + src: 'foobar.jpg', + fallback: 'https://placehold.co/40', + alt: 'fallback' + }, + + render: (args) => ({ + props: args, + template: `` + }) +};