diff --git a/client/src/app/modules/ascent/ascent-list/ascent-list.component.html b/client/src/app/modules/ascent/ascent-list/ascent-list.component.html index a16d53ef..49e08b26 100644 --- a/client/src/app/modules/ascent/ascent-list/ascent-list.component.html +++ b/client/src/app/modules/ascent/ascent-list/ascent-list.component.html @@ -166,7 +166,6 @@ [user]="ascent.createdBy" styleClass="mr-2" size="large" - shape="circle" /> }
@@ -438,7 +436,6 @@ [user]="ascent.createdBy" styleClass="mr-3" size="large" - shape="circle" /> }
diff --git a/client/src/app/modules/comments/comment/comment.component.html b/client/src/app/modules/comments/comment/comment.component.html index a40339f2..ca8137be 100644 --- a/client/src/app/modules/comments/comment/comment.component.html +++ b/client/src/app/modules/comments/comment/comment.component.html @@ -4,7 +4,6 @@ [user]="comment.createdBy" [imageFallback]="comment.isDeleted ? 'assets/user.png' : null" size="large" - shape="circle" />
diff --git a/client/src/app/modules/core/menu/menu.component.html b/client/src/app/modules/core/menu/menu.component.html index 5c340297..cbca1cbd 100644 --- a/client/src/app/modules/core/menu/menu.component.html +++ b/client/src/app/modules/core/menu/menu.component.html @@ -49,7 +49,6 @@ data-cy="auth-menu-button" styleClass="mr-2" size="normal" - shape="circle" /> } diff --git a/client/src/app/modules/core/searchable/searchable.component.html b/client/src/app/modules/core/searchable/searchable.component.html index 9316c95a..4c30c3f2 100644 --- a/client/src/app/modules/core/searchable/searchable.component.html +++ b/client/src/app/modules/core/searchable/searchable.component.html @@ -57,7 +57,6 @@ " styleClass="mr-2" size="normal" - shape="circle" >
{{ searchable.area.name }}
@@ -92,7 +91,6 @@ " styleClass="mr-2" size="normal" - shape="circle" >
{{ searchable.sector.name }}
@@ -122,7 +120,6 @@ " styleClass="mr-2" size="normal" - shape="circle" >
{{ searchable.crag.name }}
@@ -143,7 +140,6 @@ [user]="searchable.user" styleClass="mr-2" size="normal" - shape="circle" />
{{ searchable.user.fullname }}
diff --git a/client/src/app/modules/ranking/ranking-list/ranking-list.component.html b/client/src/app/modules/ranking/ranking-list/ranking-list.component.html index c81dec7b..1f3dce35 100644 --- a/client/src/app/modules/ranking/ranking-list/ranking-list.component.html +++ b/client/src/app/modules/ranking/ranking-list/ranking-list.component.html @@ -123,7 +123,6 @@ [user]="ranking.user" styleClass="mr-2" size="large" - shape="circle" /> + + + + +} @else if (imageUrl) { @@ -11,7 +24,7 @@ [label]="initials" [style]="gradientStyle" [size]="size" - [shape]="shape" + shape="circle" [styleClass]="fallbackClasses" [attr.aria-label]="ariaLabel" /> diff --git a/client/src/app/modules/shared/components/user-avatar/user-avatar.component.scss b/client/src/app/modules/shared/components/user-avatar/user-avatar.component.scss index 054ff1fa..07dc845b 100644 --- a/client/src/app/modules/shared/components/user-avatar/user-avatar.component.scss +++ b/client/src/app/modules/shared/components/user-avatar/user-avatar.component.scss @@ -3,3 +3,43 @@ font-weight: 600; border: none; } + +:host ::ng-deep p-image.lc-user-avatar-preview { + display: inline-flex; + width: fit-content; + cursor: pointer; + line-height: 0; + position: relative; + border-radius: 50%; + overflow: hidden; + + > img { + display: block; + object-fit: cover; + border-radius: 50%; + } + + &.lc-user-avatar-preview--normal > img { + width: 2rem; + height: 2rem; + } + + &.lc-user-avatar-preview--large > img { + width: 3rem; + height: 3rem; + } + + &.lc-user-avatar-preview--xlarge > img { + width: 4rem; + height: 4rem; + } + + .p-image-preview-mask { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; + border-radius: 50%; + margin: 0; + } +} diff --git a/client/src/app/modules/shared/components/user-avatar/user-avatar.component.ts b/client/src/app/modules/shared/components/user-avatar/user-avatar.component.ts index 4e63a8e6..094aa362 100644 --- a/client/src/app/modules/shared/components/user-avatar/user-avatar.component.ts +++ b/client/src/app/modules/shared/components/user-avatar/user-avatar.component.ts @@ -6,15 +6,32 @@ import { SimpleChanges, } from '@angular/core'; import { AvatarModule } from 'primeng/avatar'; +import { ImageModule } from 'primeng/image'; import { User } from '../../../../models/user'; import { userAvatarGradientCss, userInitials, } from '../../../../utility/misc/user-avatar-gradient'; +/** + * Renders a user avatar using one of three template branches: + * + * 1. `p-image` (preview) — when `[preview]` is true and the user has an avatar file. + * PrimeNG Avatar has no fullscreen preview; `p-image` provides the same click-to-zoom + * behaviour as gallery images. Both `[src]` and `[previewImageSrc]` are set to + * `thumbnailXL`: the former for the inline circle, the latter for the fullscreen overlay + * (PrimeNG falls back to `[src]` when `[previewImageSrc]` is omitted). + * + * 2. `p-avatar` with `[image]` — default when the user has an avatar and preview is off. + * Uses the configurable `thumbnail` input (usually `thumbnailS` or `thumbnailM`) so lists + * stay lightweight. + * + * 3. `p-avatar` with `[label]` — when there is no image URL (no avatar and no imageFallback). + * Shows generated initials on a deterministic gradient background. + */ @Component({ selector: 'lc-user-avatar', - imports: [AvatarModule], + imports: [AvatarModule, ImageModule], templateUrl: './user-avatar.component.html', styleUrl: './user-avatar.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -23,12 +40,16 @@ export class UserAvatarComponent implements OnChanges { @Input() user: User | null | undefined; @Input() thumbnail: 'thumbnailS' | 'thumbnailM' = 'thumbnailS'; @Input() size: 'normal' | 'large' | 'xlarge' = 'normal'; - @Input() shape: 'circle' | 'square' = 'circle'; @Input() styleClass = ''; /** When set (e.g. deleted comment), always show this image and ignore avatar / initials. */ @Input() imageFallback: string | null | undefined; + /** Opens a fullscreen image preview when the user has an avatar. */ + @Input() preview = false; imageUrl: string | null = null; + previewImageUrl: string | null = null; + previewEnabled = false; + previewStyleClass = ''; initials = ''; gradientStyle: Record = {}; fallbackClasses = ''; @@ -51,5 +72,13 @@ export class UserAvatarComponent implements OnChanges { .join(' '); this.ariaLabel = u?.fullname?.trim() || `${first} ${last}`.trim() || this.initials; + this.previewImageUrl = u?.avatar?.thumbnailXL ?? null; + this.previewEnabled = this.preview && !!this.previewImageUrl; + const previewClasses = [ + 'lc-user-avatar-preview', + `lc-user-avatar-preview--${this.size}`, + this.styleClass, + ]; + this.previewStyleClass = previewClasses.filter(Boolean).join(' '); } } diff --git a/client/src/app/modules/user/user-detail/user-detail.component.html b/client/src/app/modules/user/user-detail/user-detail.component.html index ef2b5b66..af7f59e9 100644 --- a/client/src/app/modules/user/user-detail/user-detail.component.html +++ b/client/src/app/modules/user/user-detail/user-detail.component.html @@ -6,8 +6,8 @@ } {{ user?.firstname }} {{ user?.lastname }} diff --git a/client/src/app/modules/user/user-list/user-list.component.html b/client/src/app/modules/user/user-list/user-list.component.html index 5e75e3f5..cd240e91 100644 --- a/client/src/app/modules/user/user-list/user-list.component.html +++ b/client/src/app/modules/user/user-list/user-list.component.html @@ -65,7 +65,6 @@ [user]="user" thumbnail="thumbnailM" size="xlarge" - shape="circle" styleClass="mr-2" />