Merge pull request 'Notifications' (#12) from feature/notifications into trunk
All checks were successful
Quality Check / Linting (push) Successful in 21s
All checks were successful
Quality Check / Linting (push) Successful in 21s
Reviewed-on: #12 Reviewed-by: SZUT-Rajbir <rajbir2@schule.bremen.de>
This commit is contained in:
commit
cd7de1dedc
11 changed files with 158 additions and 31 deletions
|
@ -1,3 +1,3 @@
|
||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
|
<app-notification-box></app-notification-box>
|
||||||
<router-outlet/>
|
<router-outlet/>
|
||||||
|
|
|
@ -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'
|
||||||
})
|
})
|
||||||
|
|
37
src/app/core/notification/notification.service.ts
Normal file
37
src/app/core/notification/notification.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,6 @@
|
||||||
<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 }}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
7
src/app/notification-box/notification-box.component.html
Normal file
7
src/app/notification-box/notification-box.component.html
Normal 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>
|
31
src/app/notification-box/notification-box.component.scss
Normal file
31
src/app/notification-box/notification-box.component.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/app/notification-box/notification-box.component.ts
Normal file
41
src/app/notification-box/notification-box.component.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue