Notifications #12
11 changed files with 158 additions and 31 deletions
|
@ -1,3 +1,3 @@
|
|||
<app-header></app-header>
|
||||
|
||||
<app-notification-box></app-notification-box>
|
||||
<router-outlet/>
|
||||
|
|
|
@ -2,9 +2,11 @@ import {Component} from '@angular/core';
|
|||
import {RouterOutlet} from '@angular/router';
|
||||
import {HeaderComponent} from '@app/header/header.component';
|
||||
|
||||
import { NotificationBoxComponent } from './notification-box/notification-box.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, HeaderComponent],
|
||||
imports: [RouterOutlet, HeaderComponent, NotificationBoxComponent],
|
||||
templateUrl: './app.component.html',
|
||||
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>
|
||||
}
|
||||
</nav>
|
||||
|
||||
|
||||
@if (auth.$user|async; as user) {
|
||||
<a mat-button href="https://keycloak.szut.dev/auth/realms/szut/account">
|
||||
{{ user.username|titlecase }}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
z-index: 100;
|
||||
|
@ -31,3 +29,4 @@
|
|||
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>
|
||||
|
||||
<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 { MatButton } from '@angular/material/button';
|
||||
|
||||
import { NotificationService, NotificationType } from '../../core/notification/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
imports: [],
|
||||
imports: [MatButton],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.scss'
|
||||
})
|
||||
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;
|
||||
|
||||
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,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
background: var(--mat-sys-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