feat: Add profile images
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user