Notifications
All checks were successful
Quality Check / Linting (push) Successful in 23s
Quality Check / Linting (pull_request) Successful in 22s

This commit is contained in:
Dominik Säume 2024-12-19 21:55:15 +01:00
parent a0f1d31523
commit cd0e269000
11 changed files with 158 additions and 31 deletions

View file

@ -1,3 +1,3 @@
<app-header></app-header> <app-header></app-header>
<app-notification-box></app-notification-box>
<router-outlet/> <router-outlet/>

View file

@ -2,9 +2,11 @@ import {Component} from '@angular/core';
import {RouterOutlet} from '@angular/router'; import {RouterOutlet} from '@angular/router';
import {HeaderComponent} from '@app/header/header.component'; import {HeaderComponent} from '@app/header/header.component';
import { NotificationBoxComponent } from './notification-box/notification-box.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, HeaderComponent], imports: [RouterOutlet, HeaderComponent, NotificationBoxComponent],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })

View file

@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
export interface Notification {
msg: string;
type: NotificationType;
}
export enum NotificationType {
Information = 'info',
Error = 'error',
}
export type NotificationCallback = (notification: Notification) => void;
@Injectable({ providedIn: 'root' })
export class NotificationService {
private readonly bus: Subject<Notification> = new Subject();
private readonly subscribers: Map<string, Subscription> = new Map<string, Subscription>();
subscribe(subscriberId: string, callback: NotificationCallback): void {
this.subscribers.set(
subscriberId,
this.bus.subscribe({
next: callback
})
);
}
publish(msg: string, type: NotificationType = NotificationType.Information) {
this.bus.next({ msg, type });
}
unsubscribe(subscriberId: string) {
this.subscribers.get(subscriberId)?.unsubscribe();
}
}

View file

@ -4,22 +4,20 @@
<mat-icon>badge</mat-icon> <mat-icon>badge</mat-icon>
EMS</a> EMS</a>
@for (route of routes; track route) { @for (route of routes; track route) {
<a mat-button href="{{ route.path }}" class="{{ route.class }}">{{ route.title }}</a> <a mat-button href="{{ route.path }}" class="{{ route.class }}">{{ route.title }}</a>
} }
</nav> </nav>
@if (auth.$user|async; as user) { @if (auth.$user|async; as user) {
<a mat-button href="https://keycloak.szut.dev/auth/realms/szut/account"> <a mat-button href="https://keycloak.szut.dev/auth/realms/szut/account">
{{ user.username|titlecase }} {{ user.username|titlecase }}
</a> </a>
<button mat-icon-button (click)="auth.logout()"> <button mat-icon-button (click)="auth.logout()">
<mat-icon>logout</mat-icon> <mat-icon>logout</mat-icon>
</button> </button>
} @else { } @else {
<button mat-button class="header__login" (click)="auth.login()"> <button mat-button class="header__login" (click)="auth.login()">
<mat-icon>login</mat-icon> <mat-icon>login</mat-icon>
Login Login
</button> </button>
} }
</mat-toolbar> </mat-toolbar>

View file

@ -1,6 +1,4 @@
.header { .header {
position: sticky;
top: 0;
margin: 0; margin: 0;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
z-index: 100; z-index: 100;
@ -31,3 +29,4 @@
min-width: fit-content; min-width: fit-content;
} }
} }

View file

@ -0,0 +1,7 @@
<div class="notification-box">
@for (notification of notifications.values(); track notification){
<mat-card class="notification-box__card {{notification.type}}" [@slideInOut]>
<mat-card-content>{{notification.msg}}</mat-card-content>
</mat-card>
}
</div>

View file

@ -0,0 +1,31 @@
.notification-box {
position: absolute;
z-index: 100;
right: 0;
top: 3.5rem; // Header Height
width: 15%;
min-width: 20rem;
display: flex;
flex-direction: column;
padding: 0.5rem;
gap: 0.5rem;
overflow: hidden;
&__card {
width: 100%;
box-sizing: border-box;
&.info {
color: var(--mat-sys-tertiary);
background-color: var(--mat-sys-tertiary-container);
}
&.error {
color: var(--mat-sys-error);
background-color: var(--mat-sys-error-container);
}
}
}

View file

@ -0,0 +1,41 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Component, OnInit } from '@angular/core';
import { MatCard, MatCardContent } from '@angular/material/card';
import { Notification, NotificationService } from '@core/notification/notification.service';
const NOTIFICATION_TTL = 3000;
@Component({
selector: 'app-notification-box',
imports: [MatCard, MatCardContent],
animations: [
trigger('slideInOut', [
transition(':enter', [
style({ transform: 'translateX(100%)' }),
animate('200ms ease-in-out', style({ transform: 'translateX(0)' })),
]),
transition(':leave', [animate('200ms ease-in-out', style({ transform: 'translateX(100%)' })),])
]),
],
templateUrl: './notification-box.component.html',
styleUrl: './notification-box.component.scss'
})
export class NotificationBoxComponent implements OnInit {
notifications: Map<number, Notification> = new Map();
constructor(private notificationService: NotificationService) {
}
ngOnInit(): void {
this.notificationService.subscribe('notification-box', this.onNotification.bind(this));
}
onNotification(notification: Notification): void {
const now = Date.now();
this.notifications.set(now, notification);
setTimeout(() => {
this.notifications.delete(now);
}, NOTIFICATION_TTL);
}
}

View file

@ -1 +1,4 @@
<p>dashboard works!</p> <p>dashboard works!</p>
<button mat-button (click)="testInfo()">Test Info</button>
<button mat-button (click)="testError()">Test Error</button>

View file

@ -1,11 +1,20 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { NotificationService, NotificationType } from '../../core/notification/notification.service';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
imports: [], imports: [MatButton],
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss' styleUrl: './dashboard.component.scss'
}) })
export class DashboardComponent { export class DashboardComponent {
constructor(private notifications: NotificationService) { }
testInfo() {
this.notifications.publish('Cake', NotificationType.Information);
}
} testError() {
this.notifications.publish('Cake', NotificationType.Error);
}}

View file

@ -1,21 +1,21 @@
@use '@angular/material' as mat; @use '@angular/material' as mat;
html {
color-scheme: light dark;
@include mat.theme((color: mat.$azure-palette,
typography: Roboto,
density: 0));
--mat-sys-primary-overlay: color-mix(in srgb, var(--mat-sys-primary) 10%, transparent);
}
html, html,
body { body {
height: 100%; height: 100%;
}
body {
margin: 0; margin: 0;
background: var(--mat-sys-surface); background: var(--mat-sys-surface);
color: var(--mat-sys-on-surface); color: var(--mat-sys-on-surface);
--mat-sys-primary-overlay: color-mix(in srgb, var(--mat-sys-primary) 10%, transparent);
color-scheme: light dark;
font-family: var(--mat-sys-label-medium-font);
@include mat.theme((color: mat.$azure-palette,
typography: Roboto,
density: 0));
} }