autowp/autowp-frontend

View on GitHub
src/app/account/profile/profile.component.ts

Summary

Maintainability
A
0 mins
Test Coverage
import {AsyncPipe} from '@angular/common';
import {HttpClient, HttpErrorResponse} from '@angular/common/http';
import {ChangeDetectionStrategy, Component, ElementRef, inject, OnInit, signal, viewChild} from '@angular/core';
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {environment} from '@environment/environment';
import {APIMeRequest, APIUser, DeleteUserPhotoRequest, UpdateUserRequest, UserFields} from '@grpc/spec.pb';
import {UsersClient} from '@grpc/spec.pbsc';
import {GrpcStatusEvent} from '@ngx-grpc/common';
import {FieldMask} from '@ngx-grpc/well-known-types';
import {AuthService} from '@services/auth.service';
import {LanguageService} from '@services/language';
import {PageEnvService} from '@services/page-env.service';
import {TimezoneService} from '@services/timezone';
import {InvalidParams, InvalidParamsPipe} from '@utils/invalid-params.pipe';
import {MarkdownComponent} from '@utils/markdown/markdown.component';
import Keycloak from 'keycloak-js';
import {BehaviorSubject, combineLatest, EMPTY, Observable, of} from 'rxjs';
import {catchError, map, shareReplay, switchMap, tap} from 'rxjs/operators';
 
import {extractFieldViolations, fieldViolations2InvalidParams} from '../../grpc';
import {ToastsService} from '../../toasts/toasts.service';
 
interface FormControls {
language: FormControl<string>;
timezone: FormControl<string>;
}
 
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MarkdownComponent, FormsModule, AsyncPipe, InvalidParamsPipe, ReactiveFormsModule],
selector: 'app-account-profile',
templateUrl: './profile.component.html',
})
export class AccountProfileComponent implements OnInit {
readonly #http = inject(HttpClient);
readonly #languageService = inject(LanguageService);
readonly #keycloak = inject(Keycloak);
readonly #auth = inject(AuthService);
readonly #pageEnv = inject(PageEnvService);
readonly #timezone = inject(TimezoneService);
readonly #toastService = inject(ToastsService);
readonly #usersClient = inject(UsersClient);
 
protected settingsInvalidParams = signal<InvalidParams>({});
protected photoInvalidParams = signal<InvalidParams>({});
 
private readonly input = viewChild<ElementRef<HTMLInputElement>>('input');
 
protected readonly changeProfileUrl =
environment.keycloak.url.replace(/\/$/g, '') + '/realms/' + environment.keycloak.realm + '/account/#/personal-info';
 
protected readonly timezones$ = this.#timezone.timezones$;
 
protected readonly languages: {name: string; value: string}[] = environment.languages.map((language) => ({
name: language.name,
value: language.code,
}));
 
readonly #reload$ = new BehaviorSubject<void>(void 0);
 
protected readonly user$ = combineLatest([this.#auth.user$, this.#reload$]).pipe(
switchMap(([user]) => {
if (!user) {
this.#keycloak.login({
locale: this.#languageService.language,
redirectUri: window.location.href,
});
return EMPTY;
}
 
return of(user);
}),
switchMap(() =>
this.#usersClient.me(
new APIMeRequest({
fields: new UserFields({
img: true,
language: true,
timezone: true,
votesLeft: true,
votesPerDay: true,
}),
}),
),
),
catchError((response: unknown) => {
this.#toastService.handleError(response);
return EMPTY;
}),
shareReplay({bufferSize: 1, refCount: false}),
);
 
protected readonly votesPerDay$ = this.user$.pipe(map((user) => +user.votesPerDay || 0));
protected readonly votesLeft$ = this.user$.pipe(map((user) => +user.votesLeft || 0));
protected readonly photo$ = this.user$.pipe(map((user) => user.img));
 
protected readonly form$: Observable<FormGroup<FormControls>> = this.user$.pipe(
map(
(user) =>
new FormGroup({
language: new FormControl<string>(user.language, {nonNullable: true}),
timezone: new FormControl<string>(user.timezone, {nonNullable: true}),
}),
),
);
 
ngOnInit(): void {
setTimeout(() => this.#pageEnv.set({pageId: 129}), 0);
}
 
private showSavedMessage() {
this.#toastService.success($localize`Data saved`);
}
 
protected sendSettings(form: FormGroup<FormControls>, id: string) {
this.settingsInvalidParams.set({});
 
this.#usersClient
.updateUser(
new UpdateUserRequest({
updateMask: new FieldMask({paths: ['language', 'timezone']}),
user: new APIUser({
id,
language: form.controls.language.value || undefined,
timezone: form.controls.timezone.value || undefined,
}),
}),
)
.subscribe({
error: (response: unknown) => {
if (response instanceof GrpcStatusEvent) {
const fieldViolations = extractFieldViolations(response);
this.settingsInvalidParams.set(fieldViolations2InvalidParams(fieldViolations));
} else {
this.#toastService.handleError(response);
}
},
next: () => {
this.showSavedMessage();
},
});
}
 
protected resetPhoto(id: string) {
this.#usersClient.deleteUserPhoto(new DeleteUserPhotoRequest({id})).subscribe({
error: (response: unknown) => this.#toastService.handleError(response),
next: () => {
this.#reload$.next(void 0);
},
});
}
 
protected onChange(user: APIUser, event: Event) {
const files = [].slice.call((event.target as HTMLInputElement).files);
if (files.length <= 0) {
return;
}
 
const file = files[0];
 
const formData: FormData = new FormData();
formData.append('photo', file);
 
return this.#http
.request('POST', '/api/user/' + user.id + '/photo', {body: formData})
.pipe(
catchError((response: unknown) => {
const input = this.input();
if (input) {
input.nativeElement.value = '';
}
console.log(response);
if (response instanceof HttpErrorResponse && response.status === 400) {
console.log('ZZZZZZZZZZZ');
this.photoInvalidParams.set(response.error.invalid_params);
return EMPTY;
}
 
this.#toastService.handleError(response);
return EMPTY;
}),
tap(() => {
const input = this.input();
if (input) {
input.nativeElement.value = '';
}
}),
switchMap(() => this.#usersClient.me(new APIMeRequest({fields: new UserFields({img: true})}))),
catchError((response: unknown) => {
this.#toastService.handleError(response);
return EMPTY;
}),
tap(() => {
this.#reload$.next();
}),
)
.subscribe();
}
}