feat: Add user metadata changing display name and description with sync
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s

This commit is contained in:
2026-04-17 22:04:18 +02:00
parent 3ba8a2c9eb
commit bd21568726
41 changed files with 1176 additions and 191 deletions

View File

@@ -1,5 +1,6 @@
import {
Component,
effect,
inject,
signal
} from '@angular/core';
@@ -37,6 +38,9 @@ export class ProfileCardComponent {
readonly avatarAccept = PROFILE_AVATAR_ACCEPT_ATTRIBUTE;
readonly avatarError = signal<string | null>(null);
readonly avatarSaving = signal(false);
readonly editingField = signal<'displayName' | 'description' | null>(null);
readonly displayNameDraft = signal('');
readonly descriptionDraft = signal('');
readonly statusOptions: { value: UserStatus | null; label: string; color: string }[] = [
{ value: null, label: 'Online', color: 'bg-green-500' },
@@ -49,6 +53,19 @@ export class ProfileCardComponent {
private readonly store = inject(Store);
private readonly profileAvatar = inject(ProfileAvatarFacade);
private readonly profileAvatarEditor = inject(ProfileAvatarEditorService);
private readonly syncProfileDrafts = effect(() => {
const user = this.user();
const editingField = this.editingField();
if (editingField !== 'displayName') {
this.displayNameDraft.set(user.displayName || '');
}
if (editingField !== 'description') {
this.descriptionDraft.set(user.description || '');
}
}, { allowSignalWrites: true });
currentStatusColor(): string {
switch (this.user().status) {
@@ -81,6 +98,31 @@ export class ProfileCardComponent {
this.showStatusMenu.set(false);
}
onDisplayNameInput(event: Event): void {
this.displayNameDraft.set((event.target as HTMLInputElement).value);
}
onDescriptionInput(event: Event): void {
this.descriptionDraft.set((event.target as HTMLTextAreaElement).value);
}
startEdit(field: 'displayName' | 'description'): void {
if (!this.editable() || this.editingField() === field) {
return;
}
this.editingField.set(field);
}
finishEdit(field: 'displayName' | 'description'): void {
if (this.editingField() !== field) {
return;
}
this.commitProfileDrafts();
this.editingField.set(null);
}
pickAvatar(fileInput: HTMLInputElement): void {
if (!this.editable() || this.avatarSaving()) {
return;
@@ -147,4 +189,49 @@ export class ProfileCardComponent {
this.avatarSaving.set(false);
}
}
private commitProfileDrafts(): void {
if (!this.editable()) {
return;
}
const displayName = this.normalizeDisplayName(this.displayNameDraft());
if (!displayName) {
this.displayNameDraft.set(this.user().displayName || '');
return;
}
const user = this.user();
const description = this.normalizeDescription(this.descriptionDraft());
if (
displayName === this.normalizeDisplayName(user.displayName)
&& description === this.normalizeDescription(user.description)
) {
return;
}
const profile = {
displayName,
description,
profileUpdatedAt: Date.now()
};
this.store.dispatch(UsersActions.updateCurrentUserProfile({ profile }));
this.user.update((user) => ({
...user,
...profile
}));
}
private normalizeDisplayName(value: string | undefined): string {
return value?.trim().replace(/\s+/g, ' ') || '';
}
private normalizeDescription(value: string | undefined): string | undefined {
const normalized = value?.trim();
return normalized || undefined;
}
}