diff --git a/frontend/src/app/core/core.module.ts b/frontend/src/app/core/core.module.ts index 0366cabc..a84236ba 100644 --- a/frontend/src/app/core/core.module.ts +++ b/frontend/src/app/core/core.module.ts @@ -3,6 +3,7 @@ import { SharedModule } from '../shared/shared.module'; import { FooterComponent } from './footer/footer.component'; import { MenuComponent } from './menu/menu.component'; import { UserMenuComponent } from './menu/user-menu/user-menu.component'; +import { ToastContainerComponent } from './toast-container/toast-container.component'; @@ -11,6 +12,7 @@ import { UserMenuComponent } from './menu/user-menu/user-menu.component'; FooterComponent, MenuComponent, UserMenuComponent, + ToastContainerComponent, ], imports: [ SharedModule @@ -18,6 +20,7 @@ import { UserMenuComponent } from './menu/user-menu/user-menu.component'; exports: [ FooterComponent, MenuComponent, + ToastContainerComponent ] }) export class CoreModule { } diff --git a/frontend/src/app/core/toast-container/toast-container.component.html b/frontend/src/app/core/toast-container/toast-container.component.html new file mode 100644 index 00000000..12a606a4 --- /dev/null +++ b/frontend/src/app/core/toast-container/toast-container.component.html @@ -0,0 +1,11 @@ + + {{ toast.body }} + diff --git a/frontend/src/app/core/toast-container/toast-container.component.scss b/frontend/src/app/core/toast-container/toast-container.component.scss new file mode 100644 index 00000000..1c14a073 --- /dev/null +++ b/frontend/src/app/core/toast-container/toast-container.component.scss @@ -0,0 +1,7 @@ +:host { + position: fixed; + top: 0; + right: 0; + margin: 0.5em; + z-index: 1200; + } diff --git a/frontend/src/app/core/toast-container/toast-container.component.spec.ts b/frontend/src/app/core/toast-container/toast-container.component.spec.ts new file mode 100644 index 00000000..fb73b016 --- /dev/null +++ b/frontend/src/app/core/toast-container/toast-container.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToastContainerComponent } from './toast-container.component'; + +describe('ToastContainerComponent', () => { + let component: ToastContainerComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ToastContainerComponent] + }); + fixture = TestBed.createComponent(ToastContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/core/toast-container/toast-container.component.ts b/frontend/src/app/core/toast-container/toast-container.component.ts new file mode 100644 index 00000000..efe7cd72 --- /dev/null +++ b/frontend/src/app/core/toast-container/toast-container.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { ToastService } from '@services/toast.service'; + +@Component({ + selector: 'lc-toast-container', + templateUrl: './toast-container.component.html', + styleUrls: ['./toast-container.component.scss'] +}) +export class ToastContainerComponent { + constructor(public toastService: ToastService) { } +} diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 790e0f33..09641d3c 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { SessionService } from './session.service'; -import { Observable, Subject, catchError, map, of, switchMap, merge, share, startWith, withLatestFrom } from 'rxjs'; +import { Observable, Subject, catchError, map, of, switchMap, merge, share, startWith, withLatestFrom, shareReplay } from 'rxjs'; import { UserRegistration, UserResponse, UserLogin, PasswordForgotten, ResetPassword, KeyInfo, UserSettings } from '../user/models/user'; import { encodeUserData, parseUserData } from '../user/utils'; import _ from 'underscore'; @@ -121,7 +121,7 @@ export class AuthService { this.updateSettingsUser$ ).pipe( startWith(undefined), - share() + shareReplay(1) ); public isAuthenticated$ = this.currentUser$.pipe( diff --git a/frontend/src/app/services/toast.service.spec.ts b/frontend/src/app/services/toast.service.spec.ts new file mode 100644 index 00000000..e0413db8 --- /dev/null +++ b/frontend/src/app/services/toast.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ToastService } from './toast.service'; + +describe('ToastService', () => { + let service: ToastService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ToastService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/toast.service.ts b/frontend/src/app/services/toast.service.ts new file mode 100644 index 00000000..1fd385df --- /dev/null +++ b/frontend/src/app/services/toast.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; + +type ToastType = 'success' | 'info' | 'warning' | 'danger'; + +interface Toast { + header: string; + body: string; + className: string; + delay?: number; +} + +const TOAST_STYLES: Record = { + success: 'bg-success text-light', + info: 'bg-info text-dark', + warning: 'bg-warning text-dark', + danger: 'bg-danger text-light', +}; + +@Injectable({ + providedIn: 'root' +}) +export class ToastService { + public toasts: Toast[] = []; + + public show(header: string, body: string, type: ToastType = 'info', delay?: number) { + const className = TOAST_STYLES[type]; + this.toasts.push({ header, body, delay, className }); + } + + public remove(toast: Toast): void { + this.toasts = this.toasts.filter(t => t !== toast); + } + + public clear(): void { + this.toasts = []; + } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index c58dc05c..be6bfabe 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,3 +1,5 @@ +/// + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 374cc9d2..ec26f703 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -3,7 +3,9 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": [ + "@angular/localize" + ] }, "files": [ "src/main.ts" diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json index 18ce5c40..c63b6982 100644 --- a/frontend/tsconfig.spec.json +++ b/frontend/tsconfig.spec.json @@ -4,7 +4,8 @@ "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ - "jasmine" + "jasmine", + "@angular/localize" ] }, "include": [ From 64afba1b5b5f3a1e3c6980ee6a36539b556a45d1 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Sat, 4 May 2024 20:41:21 +0200 Subject: [PATCH 032/565] Add toasters --- .../toast-container.component.html | 4 +- .../toast-container.component.scss | 2 +- frontend/src/app/services/auth.service.ts | 14 ++-- frontend/src/app/services/toast.service.ts | 27 +++++++- .../src/app/user/login/login.component.html | 17 ++--- .../src/app/user/login/login.component.ts | 38 +++++++--- .../password-forgotten.component.html | 6 -- .../password-forgotten.component.ts | 40 ++++++++--- .../app/user/register/register.component.html | 5 -- .../app/user/register/register.component.ts | 15 ++-- .../reset-password.component.html | 6 -- .../reset-password.component.ts | 22 +++--- .../user-settings.component.html | 16 ----- .../user-settings/user-settings.component.ts | 55 +++++++++++---- .../verify-email/verify-email.component.html | 16 ----- .../verify-email/verify-email.component.ts | 69 +++++++++++++------ 16 files changed, 216 insertions(+), 136 deletions(-) diff --git a/frontend/src/app/core/toast-container/toast-container.component.html b/frontend/src/app/core/toast-container/toast-container.component.html index 12a606a4..2c65bb13 100644 --- a/frontend/src/app/core/toast-container/toast-container.component.html +++ b/frontend/src/app/core/toast-container/toast-container.component.html @@ -1,10 +1,10 @@ {{ toast.body }} diff --git a/frontend/src/app/core/toast-container/toast-container.component.scss b/frontend/src/app/core/toast-container/toast-container.component.scss index 1c14a073..42d1ad05 100644 --- a/frontend/src/app/core/toast-container/toast-container.component.scss +++ b/frontend/src/app/core/toast-container/toast-container.component.scss @@ -1,6 +1,6 @@ :host { position: fixed; - top: 0; + bottom: 0; right: 0; margin: 0.5em; z-index: 1200; diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 09641d3c..5d77de13 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -63,6 +63,8 @@ export class AuthService { switchMap(key => this.http.post( this.authRoute('registration/verify-email/'), { key } + ).pipe( + catchError(error => of({ error: error.error })) )), share() ); @@ -104,10 +106,10 @@ export class AuthService { ); private updateSettingsUser$ = this.updateSettingsResult$ - .pipe( - withLatestFrom(this.backendUser$), - map(([userData, currentUser]) => 'error' in userData ? currentUser : parseUserData(userData)), - ); + .pipe( + withLatestFrom(this.backendUser$), + map(([userData, currentUser]) => 'error' in userData ? currentUser : parseUserData(userData)), + ); public logout$ = new Subject(); public logoutResult$ = this.logout$.pipe( @@ -137,10 +139,12 @@ export class AuthService { ).subscribe(() => this.logout$.next()); } - public keyInfo$(key: string): Observable { + public keyInfo$(key: string): Observable { return this.http.post( this.authRoute('registration/key-info/'), { key } + ).pipe( + catchError(error => of({ error: error.error })) ); } diff --git a/frontend/src/app/services/toast.service.ts b/frontend/src/app/services/toast.service.ts index 1fd385df..333d5326 100644 --- a/frontend/src/app/services/toast.service.ts +++ b/frontend/src/app/services/toast.service.ts @@ -6,6 +6,13 @@ interface Toast { header: string; body: string; className: string; + delay: number; +} + +interface ToastInput { + header?: string; + body: string; + type?: ToastType; delay?: number; } @@ -16,15 +23,29 @@ const TOAST_STYLES: Record = { danger: 'bg-danger text-light', }; +const TOAST_DEFAULT_HEADERS: Record = { + success: 'Success', + info: 'Info', + warning: 'Warning', + danger: 'Error', +}; + @Injectable({ providedIn: 'root' }) export class ToastService { public toasts: Toast[] = []; - public show(header: string, body: string, type: ToastType = 'info', delay?: number) { - const className = TOAST_STYLES[type]; - this.toasts.push({ header, body, delay, className }); + public show(toastInput: ToastInput): Toast { + const type = toastInput.type || 'info'; + const toast: Toast = { + className: TOAST_STYLES[type], + header: toastInput.header || TOAST_DEFAULT_HEADERS[type], + body: toastInput.body, + delay: toastInput.delay || 5000, + } + this.toasts.push(toast); + return toast; } public remove(toast: Toast): void { diff --git a/frontend/src/app/user/login/login.component.html b/frontend/src/app/user/login/login.component.html index 020e4f97..5e6f4d63 100644 --- a/frontend/src/app/user/login/login.component.html +++ b/frontend/src/app/user/login/login.component.html @@ -13,8 +13,9 @@

Sign in

class="form-control" [class.is-invalid]="form.controls.username.touched && form.controls.username.invalid" [formControl]="form.controls.username"> -

Username is required.

-

Invalid username.

+

+ {{ error }} +

+

+ {{ error }} +

- -