feat: notifications
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
|
<app-toast-container></app-toast-container>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap/nav';
|
} from '@ng-bootstrap/ng-bootstrap/nav';
|
||||||
import { filter } from 'rxjs';
|
import { filter } from 'rxjs';
|
||||||
import { APIService } from './services/api';
|
import { APIService } from './services/api';
|
||||||
|
import { ToastContainer } from "./components/toast-container/toast-container";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet, NgbModule, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLinkBase, RouterLinkWithHref],
|
imports: [RouterOutlet, NgbModule, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLinkBase, RouterLinkWithHref, ToastContainer],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.less'
|
styleUrl: './app.less'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="toast-container position-fixed top-0 end-0 p-3">
|
||||||
|
@for (notification of notifications.notifications(); track notification) {
|
||||||
|
<ngb-toast
|
||||||
|
[autohide]="true"
|
||||||
|
[delay]="notification.delay ?? 3000"
|
||||||
|
(hidden)="notifications.remove(notification)"
|
||||||
|
>
|
||||||
|
<div class="text-{{ notification.type }}">
|
||||||
|
{{ notification.message }}
|
||||||
|
</div>
|
||||||
|
</ngb-toast>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ToastContainer } from './toast-container';
|
||||||
|
|
||||||
|
describe('ToastContainer', () => {
|
||||||
|
let component: ToastContainer;
|
||||||
|
let fixture: ComponentFixture<ToastContainer>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ToastContainer],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ToastContainer);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
13
client/src/app/components/toast-container/toast-container.ts
Normal file
13
client/src/app/components/toast-container/toast-container.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { NgbToastModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { NotificationService } from '../../services/notification';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-toast-container',
|
||||||
|
imports: [NgbToastModule],
|
||||||
|
templateUrl: './toast-container.html',
|
||||||
|
styleUrl: './toast-container.less',
|
||||||
|
})
|
||||||
|
export class ToastContainer {
|
||||||
|
notifications = inject(NotificationService);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { APIService } from '../../services/api';
|
import { APIService } from '../../services/api';
|
||||||
import { GenericMessage } from '@message/Message';
|
import { GenericMessage } from '@message/Message';
|
||||||
|
import { NotificationService } from '../../services/notification';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-screen-login',
|
selector: 'app-screen-login',
|
||||||
@@ -21,6 +22,7 @@ export class ScreenLogin {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private api: APIService,
|
private api: APIService,
|
||||||
|
private notify: NotificationService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
@@ -43,7 +45,7 @@ export class ScreenLogin {
|
|||||||
},
|
},
|
||||||
error: (resp) => {
|
error: (resp) => {
|
||||||
let msg: GenericMessage = resp.error;
|
let msg: GenericMessage = resp.error;
|
||||||
this.error.set(msg.message || 'Login failed. Please try again.');
|
this.notify.error(msg.message || 'Login failed. Please try again.');
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<button class="btn btn-primary btn-lg" (click)="sendMoney()">
|
<button class="btn btn-primary btn-lg" (click)="sendMoney()">
|
||||||
Send Money
|
Send Money
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary btn-lg" (click)="cancel()">
|
<button class="btn btn-outline-secondary btn-lg" (click)="clear()">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { APIService } from '../../services/api';
|
import { APIService } from '../../services/api';
|
||||||
|
import { NotificationService } from '../../services/notification';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-screen-send',
|
selector: 'app-screen-send',
|
||||||
@@ -15,21 +16,30 @@ export class ScreenSend {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private api: APIService,
|
private api: APIService,
|
||||||
|
private notify: NotificationService,
|
||||||
){}
|
){}
|
||||||
|
|
||||||
sendMoney() {
|
sendMoney() {
|
||||||
this.api.send(this.amount, this.recipient, this.reference).subscribe({
|
this.api.send(this.amount, this.recipient, this.reference).subscribe({
|
||||||
next:()=> {
|
next:()=> {
|
||||||
this.cancel()
|
this.clear()
|
||||||
//TODO show success message
|
this.notify.success(`Sent ${this.amount} to ${this.recipient}`);
|
||||||
},
|
},
|
||||||
error:()=> {
|
error:(err)=> {
|
||||||
//TODO show error message
|
if(err.status == 404){
|
||||||
|
this.notify.error(`Invalid recipient "${this.recipient}"`);
|
||||||
|
}
|
||||||
|
else if(err.status == 402){
|
||||||
|
this.notify.error(`Insufficient funds.`);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.notify.error(`An error occurred during payment: ${err.status}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
clear() {
|
||||||
this.amount = 0;
|
this.amount = 0;
|
||||||
this.recipient = '';
|
this.recipient = '';
|
||||||
this.reference = '';
|
this.reference = '';
|
||||||
|
|||||||
16
client/src/app/services/notification.spec.ts
Normal file
16
client/src/app/services/notification.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NotificationService } from './notification';
|
||||||
|
|
||||||
|
describe('Notification', () => {
|
||||||
|
let service: NotificationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(NotificationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
41
client/src/app/services/notification.ts
Normal file
41
client/src/app/services/notification.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class NotificationService {
|
||||||
|
notifications = signal<Notification[]>([]);
|
||||||
|
|
||||||
|
show(notification: Notification) {
|
||||||
|
this.notifications.update(items => [
|
||||||
|
...items,
|
||||||
|
notification,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(notification: Notification) {
|
||||||
|
this.notifications.update(items =>
|
||||||
|
items.filter(item => item !== notification)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message: string) {
|
||||||
|
this.show({ type: 'success', message});
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string) {
|
||||||
|
this.show({ type: 'danger', message});
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string) {
|
||||||
|
this.show({ type: 'warning', message});
|
||||||
|
}
|
||||||
|
info(message: string) {
|
||||||
|
this.show({ type: 'info', message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface Notification {
|
||||||
|
type: 'success' | 'warning' | 'info' | 'danger';
|
||||||
|
message: string;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user