diff --git a/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.html b/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.html index e09f8be..d9b9d11 100644 --- a/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.html +++ b/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.html @@ -35,16 +35,6 @@ required /> -
-
-

{{modelError}}

-
-
-
-
-

{{result}}

-
-
Annuler diff --git a/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.ts b/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.ts index 608a637..15c521b 100644 --- a/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.ts +++ b/src/main/ts/src/app/account-settings/profil-edition/profil-edition.component.ts @@ -3,6 +3,7 @@ import { ProfilEditionService } from './profil-edition.service'; import { HttpEventType, HttpResponse } from '@angular/common/http'; import { User } from '../../core/entities'; import { AuthService } from '../../core/services/auth.service'; +import { NotificationsComponent } from 'src/app/core/notifications/notifications.component'; @Component({ selector: 'app-profil-edition', @@ -33,8 +34,6 @@ export class ProfilEditionComponent implements OnInit { selectedFiles: FileList; currentFileUpload: File; progress: { percentage: number } = { percentage: 0 }; - modelError: string; - result: string; constructor( private profilEditionService: ProfilEditionService, @@ -69,32 +68,21 @@ export class ProfilEditionComponent implements OnInit { console.log('File ' + result.body + ' completely uploaded!'); this.model.image = result.body as string; this.authService.setAuthenticated(this.model); - this.setMessage('Image de profil modifiée.', false); + NotificationsComponent.success('Image de profil modifiée.'); } this.selectedFiles = undefined; + }, error => { + console.log(error); + NotificationsComponent.error('Une erreur est survenue lors de l\'upload de votre image de profil.'); }); } } onSubmit(): void { this.profilEditionService.updateUser(this.model).subscribe(() => { - this.setMessage('Modification enregistrée.', false); + NotificationsComponent.success('Modification enregistrée.'); }, error => { - this.setMessage('L\'adresse mail saisie n\'est pas disponible.', true); + NotificationsComponent.error('L\'adresse mail saisie n\'est pas disponible.'); }); } - - setMessage(message: string, error: boolean): void { - this[error ? 'modelError' : 'result'] = message; - - const resultMsgDiv = document.getElementById(error ? 'errorMsg' : 'resultMsg'); - resultMsgDiv.style.maxHeight = '64px'; - - setTimeout(() => { - resultMsgDiv.style.maxHeight = '0px'; - setTimeout(() => { - this[error ? 'modelError' : 'result'] = undefined; - }, 550); - }, 3000); - } } diff --git a/src/main/ts/src/app/app.component.html b/src/main/ts/src/app/app.component.html index c22508b..eb875a3 100755 --- a/src/main/ts/src/app/app.component.html +++ b/src/main/ts/src/app/app.component.html @@ -1,4 +1,5 @@ +
diff --git a/src/main/ts/src/app/app.module.ts b/src/main/ts/src/app/app.module.ts index 8fe9db7..5a7cdbe 100755 --- a/src/main/ts/src/app/app.module.ts +++ b/src/main/ts/src/app/app.module.ts @@ -1,7 +1,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { HttpModule } from '@angular/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { RouterModule } from '@angular/router'; @@ -37,6 +36,8 @@ import { SearchComponent } from './search/search.component'; import { SigninComponent } from './signin/signin.component'; import { VersionRevisionComponent } from './version-revisions/version-revisions.component'; import { HealthCheckComponent } from './health-check/health-check.component'; +import { NotificationElement } from './core/notifications/notification-element/notification-element.component'; +import { NotificationsComponent } from './core/notifications/notifications.component'; // Reusable components import { PostCardComponent } from './core/post-card/post-card.component'; @@ -63,6 +64,8 @@ import { HealthCheckService } from './health-check/health-check.service'; @NgModule({ declarations: [ AppComponent, + NotificationsComponent, + NotificationElement, HeaderComponent, FooterComponent, LoginComponent, @@ -84,12 +87,11 @@ import { HealthCheckService } from './health-check/health-check.service'; SearchBarComponent, ProgressBarComponent, ForbiddenComponent, - HealthCheckComponent + HealthCheckComponent, ], imports: [ BrowserModule, FormsModule, - HttpModule, HttpClientModule, MDBBootstrapModule.forRoot(), RouterModule.forRoot( diff --git a/src/main/ts/src/app/core/interceptors/unauthorized.interceptor.ts b/src/main/ts/src/app/core/interceptors/unauthorized.interceptor.ts index f795113..764a89d 100644 --- a/src/main/ts/src/app/core/interceptors/unauthorized.interceptor.ts +++ b/src/main/ts/src/app/core/interceptors/unauthorized.interceptor.ts @@ -19,7 +19,7 @@ export class UnauthorizedInterceptor implements HttpInterceptor { this.router.navigate(['/login']); } - const error = err.error.message || err.statusText; + const error = (err.error && err.error.message) || err.statusText; return throwError(error); })); } diff --git a/src/main/ts/src/app/core/notifications/notification-class.ts b/src/main/ts/src/app/core/notifications/notification-class.ts new file mode 100644 index 0000000..b9a56df --- /dev/null +++ b/src/main/ts/src/app/core/notifications/notification-class.ts @@ -0,0 +1,26 @@ +/** + * Class which represents a notification class. + * It serves to set the notification appearence. + */ +export class NotificationClass { + /** + * Default constructor. + * @param {string} icon Class name of font-awsome icon. + * @param {string} clazz The class to set notification style. + */ + constructor( + public icon: string, + public clazz: string, + ) {} +} + +/** + * Constant instances of NotificationClass. + */ +export const NotificationClasses = Object.freeze({ + 'Error': new NotificationClass('exclamation-circle ', 'alert-danger'), + 'Warn': new NotificationClass('exclamation-triangle', 'alert-warning'), + 'Info': new NotificationClass('info-circle', 'alert-info'), + 'Success': new NotificationClass('check-circle', 'alert-success') +}); + diff --git a/src/main/ts/src/app/core/notifications/notification-element/notification-element.component.html b/src/main/ts/src/app/core/notifications/notification-element/notification-element.component.html new file mode 100644 index 0000000..ff63a21 --- /dev/null +++ b/src/main/ts/src/app/core/notifications/notification-element/notification-element.component.html @@ -0,0 +1,7 @@ + diff --git a/src/main/ts/src/app/core/notifications/notification-element/notification-element.component.ts b/src/main/ts/src/app/core/notifications/notification-element/notification-element.component.ts new file mode 100644 index 0000000..371e219 --- /dev/null +++ b/src/main/ts/src/app/core/notifications/notification-element/notification-element.component.ts @@ -0,0 +1,78 @@ +import { NotificationClass } from './../notification-class'; +import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; + +/** + * Class which represents a notification in the notifications list. + */ +@Component({ + selector: 'app-notification-element', + templateUrl: 'notification-element.component.html', + styles: [` + #notification { + transition: all 0.7s ease-out; + position: relative; + } + .close { + position: absolute; + right: 7px; + top: 12px; + font-size: 19px; + opacity: 0; + } + #notification:hover .close { + opacity: 0.5; + } + `] +}) +export class NotificationElement implements OnInit { + /** + * The notification model. + */ + @Input() model: NotificationModel; + /** + * The notification DOM element. + */ + @ViewChild('notification') notification: ElementRef; + + /** + * Sets the DOM element in the model object and plays with opacity. + */ + ngOnInit(): void { + this.model.notification = this.notification; + + this.notification.nativeElement.style.opacity = 0; + setTimeout(() => { + this.notification.nativeElement.style.opacity = 1; + }, 100); + } +} + +/** + * Class which represents the notification model. + */ +export class NotificationModel { + /** + * Element which represents the DOM element of the notification element. + */ + notification: ElementRef; + + /** + * Default constructor. + * @param {string} content The message of the notification. + * @param {NotificationClass} notificationClass The category of the notification (info, error...). + */ + constructor( + public content: string, + public notificationClass: NotificationClass + ) {} + + /** + * Hides the notification DOM element. + */ + public hide(): void { + this.notification.nativeElement.style.opacity = 0; + setTimeout(() => { + this.notification.nativeElement.style.display = 'none'; + }, 800); + } +} diff --git a/src/main/ts/src/app/core/notifications/notifications.component.html b/src/main/ts/src/app/core/notifications/notifications.component.html new file mode 100644 index 0000000..9765310 --- /dev/null +++ b/src/main/ts/src/app/core/notifications/notifications.component.html @@ -0,0 +1,5 @@ +
+ +
+ diff --git a/src/main/ts/src/app/core/notifications/notifications.component.ts b/src/main/ts/src/app/core/notifications/notifications.component.ts new file mode 100644 index 0000000..64e2f10 --- /dev/null +++ b/src/main/ts/src/app/core/notifications/notifications.component.ts @@ -0,0 +1,108 @@ +import { NotificationClass, NotificationClasses } from './notification-class'; +import { NotificationModel } from './notification-element/notification-element.component'; +import { Component, OnInit } from '@angular/core'; + +/** + * Class which offers the notifications service. + */ +@Component({ + selector: 'app-notifications', + templateUrl: 'notifications.component.html', + styles: [` + #notification-container { + position: fixed; + top: 50px; + right: 20px; + width: 300px; + z-index: 1100; + } + `] +}) +export class NotificationsComponent implements OnInit { + /** + * Singleton of the notification service. + */ + private static component: NotificationsComponent; + + /** + * List of notifications model. + */ + notificationList: Array = []; + + /** + * Creates an error notification. + * @param {string} message The content of the notification. + */ + public static error(message: string): void { + NotificationsComponent.notif(message, NotificationClasses.Error); + } + + /** + * Creates a warning notification. + * @param {string} message The content of the notification. + */ + public static warn(message: string): void { + NotificationsComponent.notif(message, NotificationClasses.Warn); + } + + /** + * Creates an info notification. + * @param {string} message The content of the notification. + */ + public static info(message: string): void { + NotificationsComponent.notif(message, NotificationClasses.Info); + } + + /** + * Creates a success notification. + * @param {string} message The content of the notification. + */ + public static success(message: string): void { + NotificationsComponent.notif(message, NotificationClasses.Success); + } + + /** + * Create a notification. The {@code notifClass} param defines the category of + * the notification (info, error...). + * @param {string} message The content of the notification. + * @param {NotificationClass} notifClass The category of the notification. + */ + private static notif(message: string, notifClass: NotificationClass): void { + const elem = new NotificationModel(message, notifClass); + + NotificationsComponent.component.notificationList.push(elem); + + setTimeout(() => { + elem.hide(); + + setTimeout(() => { + NotificationsComponent.clearNotificationList(); + }, 900); + }, 4500); + } + + /** + * Clears the consumed notifications in the list. + * When a notification is created, a cooldown is set to hide it after a certain time period. + * In this cooldown, the notification have only its display as {@code none}, but the + * notification isn't remove from the list. This method removes it. + */ + private static clearNotificationList(): void { + NotificationsComponent.component.notificationList.forEach(elem => { + if (elem.notification.nativeElement.style.display === 'none') { + const index = NotificationsComponent.component.notificationList.indexOf(elem); + if (index > -1) { + NotificationsComponent.component.notificationList.splice(index, 1); + } + } + }); + } + + /** + * Set the reference of the singleton here because this component + * is created at the application startup. + */ + ngOnInit(): void { + NotificationsComponent.component = this; + } +} diff --git a/src/main/ts/src/app/disconnection/disconnection.component.ts b/src/main/ts/src/app/disconnection/disconnection.component.ts index af616f1..0088183 100755 --- a/src/main/ts/src/app/disconnection/disconnection.component.ts +++ b/src/main/ts/src/app/disconnection/disconnection.component.ts @@ -1,6 +1,6 @@ +import { HttpClient } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; -import { Http } from '@angular/http'; import { AuthService } from '../core/services/auth.service'; @@ -13,7 +13,7 @@ export class DisconnectionComponent implements OnInit { constructor( private authService: AuthService, private router: Router, - private http: Http + private http: HttpClient ) {} ngOnInit(): void { diff --git a/src/main/ts/src/app/login/login.component.html b/src/main/ts/src/app/login/login.component.html index 25c8fd4..7ff0d86 100755 --- a/src/main/ts/src/app/login/login.component.html +++ b/src/main/ts/src/app/login/login.component.html @@ -30,11 +30,6 @@ required />
-
-
-

{{loginError}}

-
-
Je n'ai pas de compte diff --git a/src/main/ts/src/app/login/login.component.ts b/src/main/ts/src/app/login/login.component.ts index d371165..030b333 100755 --- a/src/main/ts/src/app/login/login.component.ts +++ b/src/main/ts/src/app/login/login.component.ts @@ -3,6 +3,7 @@ import { Router } from '@angular/router'; import { User } from '../core/entities'; import { LoginService } from './login.service'; import { AuthService } from '../core/services/auth.service'; +import { NotificationsComponent } from '../core/notifications/notifications.component'; @Component({ selector: 'app-login', @@ -26,7 +27,6 @@ import { AuthService } from '../core/services/auth.service'; }) export class LoginComponent { model: User = new User('', '', '', '', '', null, null, ''); - loginError: string; constructor( private router: Router, @@ -36,31 +36,15 @@ export class LoginComponent { onSubmit(): void { this.loginService.login(this.model).subscribe((pUser: User) => { - console.log('Login success.'); + NotificationsComponent.success('Connexion réussie.'); this.authService.setAuthenticated(pUser); this.router.navigate(['/myPosts']); }, (error) => { - if (error.status === 401) { - console.log('Login attempt failed.'); - this.setMessage('Adresse email ou mot de passe incorrect.'); + if (error === 'Unauthorized' || error.status === 401) { + NotificationsComponent.error('Adresse email ou mot de passe incorrect.'); } else { - console.error('Error during login attempt.', error); - this.setMessage('Une erreur est survenue lors de la connexion.'); + NotificationsComponent.error('Une erreur est survenue lors de la connexion.'); } }); } - - setMessage(message: string): void { - this.loginError = message; - - const resultMsgDiv = document.getElementById('errorMsg'); - resultMsgDiv.style.maxHeight = '64px'; - - setTimeout(() => { - resultMsgDiv.style.maxHeight = '0px'; - setTimeout(() => { - this.loginError = undefined; - }, 550); - }, 3000); - } } diff --git a/src/main/ts/src/app/signin/signin.component.html b/src/main/ts/src/app/signin/signin.component.html index 9491027..352dac6 100644 --- a/src/main/ts/src/app/signin/signin.component.html +++ b/src/main/ts/src/app/signin/signin.component.html @@ -56,11 +56,6 @@ [validateSuccess]="false" required /> -
-
-
-

{{errorMsg}}

-
diff --git a/src/main/ts/src/app/signin/signin.component.ts b/src/main/ts/src/app/signin/signin.component.ts index b15376c..f883b00 100644 --- a/src/main/ts/src/app/signin/signin.component.ts +++ b/src/main/ts/src/app/signin/signin.component.ts @@ -1,3 +1,4 @@ +import { NotificationsComponent } from './../core/notifications/notifications.component'; import { Component } from '@angular/core'; import { User } from '../core/entities'; import { SigninService } from './signin.service'; @@ -14,19 +15,11 @@ import { Router } from '@angular/router'; .submitFormArea { line-height: 50px; } - - #errorMsg { - max-height: 0; - overflow: hidden; - transition: max-height 0.5s ease-out; - margin: 0; - } `] }) export class SigninComponent { model: User = new User('', '', '', '', '', null, null, ''); confirmPassword: string; - errorMsg: string; constructor( private signinService: SigninService, @@ -36,34 +29,21 @@ export class SigninComponent { onSubmit(): void { if (this.confirmPassword && this.confirmPassword === this.model.password) { this.signinService.signin(this.model).subscribe(() => { + NotificationsComponent.success('Inscription réussie.'); this.router.navigate(['/login']); }, error => { // FIXME: Type the error to get the status. switch (error.status) { case 409: - this.setMessage('L\'adresse mail saisie n\'est pas disponible'); + NotificationsComponent.error('L\'adresse mail saisie n\'est pas disponible'); break; default: - this.setMessage('Une erreur est survenue lors de l\'inscription, veuillez réessayer plus tard'); + NotificationsComponent.error('Une erreur est survenue lors de l\'inscription, veuillez réessayer plus tard'); break; } }); } else { - this.setMessage('Les mots de passe saisis ne correspondent pas'); + NotificationsComponent.error('Les mots de passe saisis ne correspondent pas'); } } - - setMessage(message: string): void { - this.errorMsg = message; - - const resultMsgDiv = document.getElementById('errorMsg'); - resultMsgDiv.style.maxHeight = '64px'; - - setTimeout(() => { - resultMsgDiv.style.maxHeight = '0px'; - setTimeout(() => { - this.errorMsg = undefined; - }, 550); - }, 3000); - } }