From 1c699cfa7dd8d4d1a5dda658f68b42acc14ed986 Mon Sep 17 00:00:00 2001 From: GODrums Date: Tue, 6 Aug 2024 18:48:41 +0200 Subject: [PATCH 1/5] feat: avatar base component --- .../src/app/ui/avatar/avatar.component.html | 13 +++ webapp/src/app/ui/avatar/avatar.component.ts | 90 +++++++++++++++++++ webapp/src/app/ui/avatar/avatar.stories.ts | 84 +++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 webapp/src/app/ui/avatar/avatar.component.html create mode 100644 webapp/src/app/ui/avatar/avatar.component.ts create mode 100644 webapp/src/app/ui/avatar/avatar.stories.ts 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..dd14b9eb --- /dev/null +++ b/webapp/src/app/ui/avatar/avatar.component.html @@ -0,0 +1,13 @@ +
+ @if (canShow()) { + + } @else { + + } +
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..be78befb --- /dev/null +++ b/webapp/src/app/ui/avatar/avatar.component.ts @@ -0,0 +1,90 @@ +import { + ChangeDetectionStrategy, + 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: { + variant: { + small: 'h-6 w-6 text-xs', + medium: 'h-10 w-10', + large: 'h-14 w-14 text-lg', + }, + }, + defaultVariants: { + variant: 'medium', + }, + }, +); + +export { args, argTypes }; + +interface AvatarVariants extends VariantProps {} + +@Component({ + selector: 'app-avatar', + standalone: true, + imports: [NgOptimizedImage], + changeDetection: ChangeDetectionStrategy.OnPush, + // template: ` + // + // + // + // + // + // + // `, + templateUrl: './avatar.component.html', +}) +// export class AppAvatarComponent { +// class = input(''); +// variant = input('medium'); + +// @ContentChild(AvatarImageDirective, { static: true }) +// image: AvatarImageDirective | null = null; + +// computedClass = computed(() => +// cn(avatarVariants({ variant: this.variant() }), this.class()), +// ); +// } +export class AppAvatarComponent { + class = input(''); + variant = input('medium'); + + src = input(''); + alt = input(''); + imageClass = input(''); + fallback = input(''); + + canShow = signal(true); + + onError = () => { + if (this.fallback.length > 0) { + this.src = this.fallback; + } else { + this.canShow.set(false); + } + }; + + computedClass = computed(() => + cn(avatarVariants({ variant: this.variant() }), this.class()), + ); + + 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..ff504460 --- /dev/null +++ b/webapp/src/app/ui/avatar/avatar.stories.ts @@ -0,0 +1,84 @@ +import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; +import { AppAvatarComponent, args, argTypes } from './avatar.component'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories +const meta: Meta = { + title: 'UI/Avatar', + component: AppAvatarComponent, + tags: ['autodocs'], + args: { + ...args, + }, + argTypes: { + ...argTypes, + }, +}; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Default: Story = { + args: { + variant: 'medium', + src: 'https://i.pravatar.cc/64?img=1', + alt: 'avatar', + class: '', + }, + + render: (args) => ({ + props: args, + template: ``, + }), +}; + +export const Small: Story = { + args: { + variant: 'small', + src: 'https://placehold.co/24', + }, + + render: (args) => ({ + props: args, + template: ``, + }), +}; + +export const Medium: Story = { + args: { + variant: 'medium', + src: 'https://placehold.co/40', + }, + + render: (args) => ({ + props: args, + template: `MD`, + }), +}; + +export const Large: Story = { + args: { + variant: 'large', + src: 'https://placehold.co/56', + }, + + render: (args) => ({ + props: args, + template: `LG`, + }), +}; + +export const WithImage: Story = { + args: { + variant: 'medium', + src: 'https://i.pravatar.cc/40', + }, + + render: (args) => ({ + props: { + variant: 'outline', + size: 'icon', + }, + template: ``, + }), +}; From df98b8d049b670778c2d111584c8f8e54de62534 Mon Sep 17 00:00:00 2001 From: GODrums Date: Wed, 7 Aug 2024 14:16:27 +0200 Subject: [PATCH 2/5] fix: avatar fallback calling --- webapp/.prettierrc | 2 +- .../src/app/ui/avatar/avatar.component.html | 12 +-- webapp/src/app/ui/avatar/avatar.component.ts | 76 +++++-------------- webapp/src/app/ui/avatar/avatar.stories.ts | 64 +++++++++------- 4 files changed, 59 insertions(+), 95 deletions(-) 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 index dd14b9eb..d57806aa 100644 --- a/webapp/src/app/ui/avatar/avatar.component.html +++ b/webapp/src/app/ui/avatar/avatar.component.html @@ -1,13 +1,3 @@
- @if (canShow()) { - - } @else { - - } +
diff --git a/webapp/src/app/ui/avatar/avatar.component.ts b/webapp/src/app/ui/avatar/avatar.component.ts index be78befb..78473781 100644 --- a/webapp/src/app/ui/avatar/avatar.component.ts +++ b/webapp/src/app/ui/avatar/avatar.component.ts @@ -1,31 +1,22 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - input, - signal, -} from '@angular/core'; +import { ChangeDetectionStrategy, 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: { - variant: { - small: 'h-6 w-6 text-xs', - medium: 'h-10 w-10', - large: 'h-14 w-14 text-lg', - }, - }, - defaultVariants: { - variant: 'medium', - }, +const [avatarVariants, args, argTypes] = cva('relative flex shrink-0 overflow-hidden rounded-full', { + variants: { + variant: { + small: 'h-6 w-6 text-xs', + medium: 'h-10 w-10', + large: 'h-14 w-14 text-lg' + } }, -); + defaultVariants: { + variant: 'medium' + } +}); export { args, argTypes }; @@ -36,31 +27,8 @@ interface AvatarVariants extends VariantProps {} standalone: true, imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, - // template: ` - // - // - // - // - // - // - // `, - templateUrl: './avatar.component.html', + templateUrl: './avatar.component.html' }) -// export class AppAvatarComponent { -// class = input(''); -// variant = input('medium'); - -// @ContentChild(AvatarImageDirective, { static: true }) -// image: AvatarImageDirective | null = null; - -// computedClass = computed(() => -// cn(avatarVariants({ variant: this.variant() }), this.class()), -// ); -// } export class AppAvatarComponent { class = input(''); variant = input('medium'); @@ -68,23 +36,17 @@ export class AppAvatarComponent { src = input(''); alt = input(''); imageClass = input(''); - fallback = input(''); + fallback = input('https://placehold.co/56'); canShow = signal(true); onError = () => { - if (this.fallback.length > 0) { - this.src = this.fallback; - } else { - this.canShow.set(false); - } + this.canShow.set(false); }; - computedClass = computed(() => - cn(avatarVariants({ variant: this.variant() }), this.class()), - ); + computedClass = computed(() => cn(avatarVariants({ variant: this.variant() }), this.class())); + + computedSrc = computed(() => (this.canShow() ? this.src() : this.fallback())); - computedImageClass = computed(() => - cn('aspect-square object-cover h-full w-full', this.imageClass()), - ); + 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 index ff504460..961713ce 100644 --- a/webapp/src/app/ui/avatar/avatar.stories.ts +++ b/webapp/src/app/ui/avatar/avatar.stories.ts @@ -1,84 +1,96 @@ import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; import { AppAvatarComponent, args, argTypes } from './avatar.component'; -// More on how to set up stories at: https://storybook.js.org/docs/writing-stories const meta: Meta = { title: 'UI/Avatar', component: AppAvatarComponent, tags: ['autodocs'], args: { - ...args, + ...args }, argTypes: { - ...argTypes, - }, + ...argTypes + } }; export default meta; type Story = StoryObj; -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const Default: Story = { args: { variant: 'medium', - src: 'https://i.pravatar.cc/64?img=1', + src: 'https://i.pravatar.cc/40?img=1', alt: 'avatar', - class: '', + class: '' }, render: (args) => ({ props: args, - template: ``, - }), + template: `` + }) }; export const Small: Story = { args: { variant: 'small', - src: 'https://placehold.co/24', + src: 'https://i.pravatar.cc/24?img=1' }, render: (args) => ({ props: args, - template: ``, - }), + template: `` + }) }; export const Medium: Story = { args: { variant: 'medium', - src: 'https://placehold.co/40', + src: 'https://i.pravatar.cc/40?img=1' }, render: (args) => ({ props: args, - template: `MD`, - }), + template: `MD` + }) }; export const Large: Story = { args: { variant: 'large', - src: 'https://placehold.co/56', + src: 'https://i.pravatar.cc/56?img=1', + alt: 'avatar', + class: '' }, render: (args) => ({ props: args, - template: `LG`, - }), + template: `LG` + }) }; -export const WithImage: Story = { +export const WithRandomImage: Story = { + args: { + variant: 'large', + src: 'https://i.pravatar.cc/56', + alt: 'avatar' + }, + + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const WithFallback: Story = { args: { variant: 'medium', - src: 'https://i.pravatar.cc/40', + src: 'foobar.jpg', + fallback: 'https://placehold.co/40', + alt: 'fallback' }, render: (args) => ({ - props: { - variant: 'outline', - size: 'icon', - }, - template: ``, - }), + props: args, + template: `` + }) }; From 10a62f4e1c3a666045806fa1177d7363c6536196 Mon Sep 17 00:00:00 2001 From: GODrums Date: Wed, 7 Aug 2024 14:19:57 +0200 Subject: [PATCH 3/5] Rename variant to size --- webapp/src/app/ui/avatar/avatar.component.ts | 14 +++++++------- webapp/src/app/ui/avatar/avatar.stories.ts | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/webapp/src/app/ui/avatar/avatar.component.ts b/webapp/src/app/ui/avatar/avatar.component.ts index 78473781..7f3b0cda 100644 --- a/webapp/src/app/ui/avatar/avatar.component.ts +++ b/webapp/src/app/ui/avatar/avatar.component.ts @@ -7,14 +7,14 @@ import { NgOptimizedImage } from '@angular/common'; const [avatarVariants, args, argTypes] = cva('relative flex shrink-0 overflow-hidden rounded-full', { variants: { - variant: { - small: 'h-6 w-6 text-xs', - medium: 'h-10 w-10', - large: 'h-14 w-14 text-lg' + size: { + default: 'h-10 w-10', + sm: 'h-6 w-6 text-xs', + lg: 'h-14 w-14 text-lg' } }, defaultVariants: { - variant: 'medium' + size: 'default' } }); @@ -31,7 +31,7 @@ interface AvatarVariants extends VariantProps {} }) export class AppAvatarComponent { class = input(''); - variant = input('medium'); + size = input('default'); src = input(''); alt = input(''); @@ -44,7 +44,7 @@ export class AppAvatarComponent { this.canShow.set(false); }; - computedClass = computed(() => cn(avatarVariants({ variant: this.variant() }), this.class())); + computedClass = computed(() => cn(avatarVariants({ size: this.size() }), this.class())); computedSrc = computed(() => (this.canShow() ? this.src() : this.fallback())); diff --git a/webapp/src/app/ui/avatar/avatar.stories.ts b/webapp/src/app/ui/avatar/avatar.stories.ts index 961713ce..59ae2807 100644 --- a/webapp/src/app/ui/avatar/avatar.stories.ts +++ b/webapp/src/app/ui/avatar/avatar.stories.ts @@ -6,7 +6,8 @@ const meta: Meta = { component: AppAvatarComponent, tags: ['autodocs'], args: { - ...args + ...args, + size: 'default' }, argTypes: { ...argTypes @@ -18,7 +19,6 @@ type Story = StoryObj; export const Default: Story = { args: { - variant: 'medium', src: 'https://i.pravatar.cc/40?img=1', alt: 'avatar', class: '' @@ -32,7 +32,7 @@ export const Default: Story = { export const Small: Story = { args: { - variant: 'small', + size: 'sm', src: 'https://i.pravatar.cc/24?img=1' }, @@ -44,7 +44,7 @@ export const Small: Story = { export const Medium: Story = { args: { - variant: 'medium', + size: 'default', src: 'https://i.pravatar.cc/40?img=1' }, @@ -56,7 +56,7 @@ export const Medium: Story = { export const Large: Story = { args: { - variant: 'large', + size: 'lg', src: 'https://i.pravatar.cc/56?img=1', alt: 'avatar', class: '' @@ -70,7 +70,7 @@ export const Large: Story = { export const WithRandomImage: Story = { args: { - variant: 'large', + size: 'lg', src: 'https://i.pravatar.cc/56', alt: 'avatar' }, @@ -83,7 +83,7 @@ export const WithRandomImage: Story = { export const WithFallback: Story = { args: { - variant: 'medium', + size: 'default', src: 'foobar.jpg', fallback: 'https://placehold.co/40', alt: 'fallback' From 16daee5fafd964cf7797e593c8cd8001f2172634 Mon Sep 17 00:00:00 2001 From: GODrums Date: Wed, 7 Aug 2024 16:17:29 +0200 Subject: [PATCH 4/5] apply review suggestions --- webapp/src/app/ui/avatar/avatar.component.ts | 3 +-- webapp/src/app/ui/avatar/avatar.stories.ts | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/webapp/src/app/ui/avatar/avatar.component.ts b/webapp/src/app/ui/avatar/avatar.component.ts index 7f3b0cda..a08d8c4e 100644 --- a/webapp/src/app/ui/avatar/avatar.component.ts +++ b/webapp/src/app/ui/avatar/avatar.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core'; +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'; @@ -26,7 +26,6 @@ interface AvatarVariants extends VariantProps {} selector: 'app-avatar', standalone: true, imports: [NgOptimizedImage], - changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './avatar.component.html' }) export class AppAvatarComponent { diff --git a/webapp/src/app/ui/avatar/avatar.stories.ts b/webapp/src/app/ui/avatar/avatar.stories.ts index 59ae2807..2aadf187 100644 --- a/webapp/src/app/ui/avatar/avatar.stories.ts +++ b/webapp/src/app/ui/avatar/avatar.stories.ts @@ -6,8 +6,7 @@ const meta: Meta = { component: AppAvatarComponent, tags: ['autodocs'], args: { - ...args, - size: 'default' + ...args }, argTypes: { ...argTypes @@ -50,7 +49,7 @@ export const Medium: Story = { render: (args) => ({ props: args, - template: `MD` + template: `` }) }; @@ -64,7 +63,7 @@ export const Large: Story = { render: (args) => ({ props: args, - template: `LG` + template: `` }) }; From 797883a88878d48daec756100c92a7a0d39d842d Mon Sep 17 00:00:00 2001 From: GODrums Date: Wed, 7 Aug 2024 16:23:00 +0200 Subject: [PATCH 5/5] fix: make avatar src required --- webapp/src/app/ui/avatar/avatar.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/app/ui/avatar/avatar.component.ts b/webapp/src/app/ui/avatar/avatar.component.ts index a08d8c4e..d2eb5a09 100644 --- a/webapp/src/app/ui/avatar/avatar.component.ts +++ b/webapp/src/app/ui/avatar/avatar.component.ts @@ -32,7 +32,7 @@ export class AppAvatarComponent { class = input(''); size = input('default'); - src = input(''); + src = input.required(); alt = input(''); imageClass = input(''); fallback = input('https://placehold.co/56');