Add new notification component.

This commit is contained in:
2019-08-04 21:45:25 +02:00
parent 9adc036e38
commit acf86dcc72
15 changed files with 250 additions and 91 deletions

View File

@@ -35,16 +35,6 @@
required /> required />
<label for="email">Email</label> <label for="email">Email</label>
</div> </div>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{modelError}}</p>
</div>
</div>
<div id="resultMsg" class="card green lighten-2 text-center z-depth-2" >
<div class="card-body">
<p class="white-text mb-0">{{result}}</p>
</div>
</div>
<div class="col submitFormArea"> <div class="col submitFormArea">
<a routerLink="/accountSettings" class="indigo-text"> <a routerLink="/accountSettings" class="indigo-text">
Annuler Annuler

View File

@@ -3,6 +3,7 @@ import { ProfilEditionService } from './profil-edition.service';
import { HttpEventType, HttpResponse } from '@angular/common/http'; import { HttpEventType, HttpResponse } from '@angular/common/http';
import { User } from '../../core/entities'; import { User } from '../../core/entities';
import { AuthService } from '../../core/services/auth.service'; import { AuthService } from '../../core/services/auth.service';
import { NotificationsComponent } from 'src/app/core/notifications/notifications.component';
@Component({ @Component({
selector: 'app-profil-edition', selector: 'app-profil-edition',
@@ -33,8 +34,6 @@ export class ProfilEditionComponent implements OnInit {
selectedFiles: FileList; selectedFiles: FileList;
currentFileUpload: File; currentFileUpload: File;
progress: { percentage: number } = { percentage: 0 }; progress: { percentage: number } = { percentage: 0 };
modelError: string;
result: string;
constructor( constructor(
private profilEditionService: ProfilEditionService, private profilEditionService: ProfilEditionService,
@@ -69,32 +68,21 @@ export class ProfilEditionComponent implements OnInit {
console.log('File ' + result.body + ' completely uploaded!'); console.log('File ' + result.body + ' completely uploaded!');
this.model.image = result.body as string; this.model.image = result.body as string;
this.authService.setAuthenticated(this.model); this.authService.setAuthenticated(this.model);
this.setMessage('Image de profil modifiée.', false); NotificationsComponent.success('Image de profil modifiée.');
} }
this.selectedFiles = undefined; this.selectedFiles = undefined;
}, error => {
console.log(error);
NotificationsComponent.error('Une erreur est survenue lors de l\'upload de votre image de profil.');
}); });
} }
} }
onSubmit(): void { onSubmit(): void {
this.profilEditionService.updateUser(this.model).subscribe(() => { this.profilEditionService.updateUser(this.model).subscribe(() => {
this.setMessage('Modification enregistrée.', false); NotificationsComponent.success('Modification enregistrée.');
}, error => { }, 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);
}
} }

View File

@@ -1,4 +1,5 @@
<app-header></app-header> <app-header></app-header>
<app-notifications></app-notifications>
<main class="container"> <main class="container">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>

View File

@@ -1,7 +1,6 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@@ -37,6 +36,8 @@ import { SearchComponent } from './search/search.component';
import { SigninComponent } from './signin/signin.component'; import { SigninComponent } from './signin/signin.component';
import { VersionRevisionComponent } from './version-revisions/version-revisions.component'; import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
import { HealthCheckComponent } from './health-check/health-check.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 // Reusable components
import { PostCardComponent } from './core/post-card/post-card.component'; import { PostCardComponent } from './core/post-card/post-card.component';
@@ -63,6 +64,8 @@ import { HealthCheckService } from './health-check/health-check.service';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
NotificationsComponent,
NotificationElement,
HeaderComponent, HeaderComponent,
FooterComponent, FooterComponent,
LoginComponent, LoginComponent,
@@ -84,12 +87,11 @@ import { HealthCheckService } from './health-check/health-check.service';
SearchBarComponent, SearchBarComponent,
ProgressBarComponent, ProgressBarComponent,
ForbiddenComponent, ForbiddenComponent,
HealthCheckComponent HealthCheckComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
FormsModule, FormsModule,
HttpModule,
HttpClientModule, HttpClientModule,
MDBBootstrapModule.forRoot(), MDBBootstrapModule.forRoot(),
RouterModule.forRoot( RouterModule.forRoot(

View File

@@ -19,7 +19,7 @@ export class UnauthorizedInterceptor implements HttpInterceptor {
this.router.navigate(['/login']); this.router.navigate(['/login']);
} }
const error = err.error.message || err.statusText; const error = (err.error && err.error.message) || err.statusText;
return throwError(error); return throwError(error);
})); }));
} }

View File

@@ -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')
});

View File

@@ -0,0 +1,7 @@
<div id="notification" #notification class="alert {{model.notificationClass.clazz}}" role="alert">
<mdb-icon fas [icon]="model.notificationClass.icon"></mdb-icon> {{model.content}}
<span id="close">
<i class="fa fa-window-close fas close"
(click)="model.hide()"></i>
</span>
</div>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,5 @@
<div id="notification-container">
<app-notification-element *ngFor="let notification of notificationList"
[model]="notification"></app-notification-element>
</div>

View File

@@ -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<NotificationModel> = [];
/**
* 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;
}
}

View File

@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Http } from '@angular/http';
import { AuthService } from '../core/services/auth.service'; import { AuthService } from '../core/services/auth.service';
@@ -13,7 +13,7 @@ export class DisconnectionComponent implements OnInit {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private router: Router, private router: Router,
private http: Http private http: HttpClient
) {} ) {}
ngOnInit(): void { ngOnInit(): void {

View File

@@ -30,11 +30,6 @@
required /> required />
<label for="password">Mot de passe</label> <label for="password">Mot de passe</label>
</div> </div>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{loginError}}</p>
</div>
</div>
<div class="col submitFormArea"> <div class="col submitFormArea">
<a routerLink="/signin" class="indigo-text"> <a routerLink="/signin" class="indigo-text">
Je n'ai pas de compte Je n'ai pas de compte

View File

@@ -3,6 +3,7 @@ import { Router } from '@angular/router';
import { User } from '../core/entities'; import { User } from '../core/entities';
import { LoginService } from './login.service'; import { LoginService } from './login.service';
import { AuthService } from '../core/services/auth.service'; import { AuthService } from '../core/services/auth.service';
import { NotificationsComponent } from '../core/notifications/notifications.component';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@@ -26,7 +27,6 @@ import { AuthService } from '../core/services/auth.service';
}) })
export class LoginComponent { export class LoginComponent {
model: User = new User('', '', '', '', '', null, null, ''); model: User = new User('', '', '', '', '', null, null, '');
loginError: string;
constructor( constructor(
private router: Router, private router: Router,
@@ -36,31 +36,15 @@ export class LoginComponent {
onSubmit(): void { onSubmit(): void {
this.loginService.login(this.model).subscribe((pUser: User) => { this.loginService.login(this.model).subscribe((pUser: User) => {
console.log('Login success.'); NotificationsComponent.success('Connexion réussie.');
this.authService.setAuthenticated(pUser); this.authService.setAuthenticated(pUser);
this.router.navigate(['/myPosts']); this.router.navigate(['/myPosts']);
}, (error) => { }, (error) => {
if (error.status === 401) { if (error === 'Unauthorized' || error.status === 401) {
console.log('Login attempt failed.'); NotificationsComponent.error('Adresse email ou mot de passe incorrect.');
this.setMessage('Adresse email ou mot de passe incorrect.');
} else { } else {
console.error('Error during login attempt.', error); NotificationsComponent.error('Une erreur est survenue lors de la connexion.');
this.setMessage('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);
}
} }

View File

@@ -56,11 +56,6 @@
[validateSuccess]="false" [validateSuccess]="false"
required /> required />
<label for="confirmPassword">Confirmez votre mot de passe</label> <label for="confirmPassword">Confirmez votre mot de passe</label>
</div>
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
<div class="card-body">
<p class="white-text mb-0">{{errorMsg}}</p>
</div>
</div> </div>
<div class="col submitFormArea"> <div class="col submitFormArea">
<a routerLink="/login" class="indigo-text"> <a routerLink="/login" class="indigo-text">

View File

@@ -1,3 +1,4 @@
import { NotificationsComponent } from './../core/notifications/notifications.component';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { User } from '../core/entities'; import { User } from '../core/entities';
import { SigninService } from './signin.service'; import { SigninService } from './signin.service';
@@ -14,19 +15,11 @@ import { Router } from '@angular/router';
.submitFormArea { .submitFormArea {
line-height: 50px; line-height: 50px;
} }
#errorMsg {
max-height: 0;
overflow: hidden;
transition: max-height 0.5s ease-out;
margin: 0;
}
`] `]
}) })
export class SigninComponent { export class SigninComponent {
model: User = new User('', '', '', '', '', null, null, ''); model: User = new User('', '', '', '', '', null, null, '');
confirmPassword: string; confirmPassword: string;
errorMsg: string;
constructor( constructor(
private signinService: SigninService, private signinService: SigninService,
@@ -36,34 +29,21 @@ export class SigninComponent {
onSubmit(): void { onSubmit(): void {
if (this.confirmPassword && this.confirmPassword === this.model.password) { if (this.confirmPassword && this.confirmPassword === this.model.password) {
this.signinService.signin(this.model).subscribe(() => { this.signinService.signin(this.model).subscribe(() => {
NotificationsComponent.success('Inscription réussie.');
this.router.navigate(['/login']); this.router.navigate(['/login']);
}, error => { }, error => {
// FIXME: Type the error to get the status. // FIXME: Type the error to get the status.
switch (error.status) { switch (error.status) {
case 409: case 409:
this.setMessage('L\'adresse mail saisie n\'est pas disponible'); NotificationsComponent.error('L\'adresse mail saisie n\'est pas disponible');
break; break;
default: 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; break;
} }
}); });
} else { } 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);
}
} }