feat: notifications

This commit is contained in:
eneller
2026-07-23 16:29:03 +02:00
parent c282e275ef
commit cb9728a90b
11 changed files with 127 additions and 8 deletions

View File

@@ -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>

View File

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

View File

@@ -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>

View File

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

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

View File

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

View File

@@ -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>

View File

@@ -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 = '';

View 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();
});
});

View 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;
}