feat: Add profile images

This commit is contained in:
2026-04-17 03:05:47 +02:00
parent 35b616fb77
commit 17738ec484
49 changed files with 2622 additions and 89 deletions

View File

@@ -4,11 +4,20 @@ import {
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideChevronDown } from '@ng-icons/lucide';
import { UserAvatarComponent } from '../user-avatar/user-avatar.component';
import { UserStatusService } from '../../../core/services/user-status.service';
import { User, UserStatus } from '../../../shared-kernel';
import {
EditableProfileAvatarSource,
ProfileAvatarFacade,
ProfileAvatarEditorService,
PROFILE_AVATAR_ACCEPT_ATTRIBUTE,
ProcessedProfileAvatar
} from '../../../domains/profile-avatar';
import { UsersActions } from '../../../store/users/users.actions';
@Component({
selector: 'app-profile-card',
@@ -25,6 +34,9 @@ export class ProfileCardComponent {
readonly user = signal<User>({ id: '', oderId: '', username: '', displayName: '', status: 'offline', role: 'member', joinedAt: 0 });
readonly editable = signal(false);
readonly showStatusMenu = signal(false);
readonly avatarAccept = PROFILE_AVATAR_ACCEPT_ATTRIBUTE;
readonly avatarError = signal<string | null>(null);
readonly avatarSaving = signal(false);
readonly statusOptions: { value: UserStatus | null; label: string; color: string }[] = [
{ value: null, label: 'Online', color: 'bg-green-500' },
@@ -34,6 +46,9 @@ export class ProfileCardComponent {
];
private readonly userStatus = inject(UserStatusService);
private readonly store = inject(Store);
private readonly profileAvatar = inject(ProfileAvatarFacade);
private readonly profileAvatarEditor = inject(ProfileAvatarEditorService);
currentStatusColor(): string {
switch (this.user().status) {
@@ -65,4 +80,71 @@ export class ProfileCardComponent {
this.userStatus.setManualStatus(status);
this.showStatusMenu.set(false);
}
pickAvatar(fileInput: HTMLInputElement): void {
if (!this.editable() || this.avatarSaving()) {
return;
}
this.avatarError.set(null);
fileInput.click();
}
async onAvatarSelected(event: Event): Promise<void> {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
let source: EditableProfileAvatarSource | null = null;
input.value = '';
if (!file) {
return;
}
const validationError = this.profileAvatar.validateFile(file);
if (validationError) {
this.avatarError.set(validationError);
return;
}
try {
source = await this.profileAvatar.prepareEditableSource(file);
const avatar = await this.profileAvatarEditor.open(source);
if (!avatar) {
return;
}
await this.applyAvatar(avatar);
} catch {
this.avatarError.set('Failed to open selected image.');
} finally {
this.profileAvatar.releaseEditableSource(source);
}
}
async applyAvatar(avatar: ProcessedProfileAvatar): Promise<void> {
const currentUser = this.user();
this.avatarSaving.set(true);
this.avatarError.set(null);
try {
await this.profileAvatar.persistProcessedAvatar(currentUser, avatar);
const updates = this.profileAvatar.buildAvatarUpdates(avatar);
this.store.dispatch(UsersActions.updateCurrentUserAvatar({ avatar: updates }));
this.user.update((user) => ({
...user,
...updates
}));
} catch {
this.avatarError.set('Failed to save profile image.');
} finally {
this.avatarSaving.set(false);
}
}
}