diff --git a/src/app/app.component.html b/src/app/app.component.html
index aa0c257..91cca96 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,4 +1,7 @@
+@defer (when bannerService.banners.length > 0) {
+
+}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 0968b09..1a54425 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -6,17 +6,20 @@ import {EmbedService} from "./services/embed.service";
import {FaIconLibrary} from "@fortawesome/angular-fontawesome";
import {fas} from "@fortawesome/free-solid-svg-icons";
import {AuthenticationService} from "./api/authentication.service";
+import {PopupBannerContainerComponent} from "./banners/popup-banner-container.component";
+import {BannerService} from "./banners/banner.service";
@Component({
selector: 'app-root',
standalone: true,
- imports: [RouterOutlet, HeaderComponent],
+ imports: [RouterOutlet, HeaderComponent, PopupBannerContainerComponent],
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(private title: TitleService,
private embed: EmbedService,
private auth: AuthenticationService,
+ protected bannerService: BannerService,
library: FaIconLibrary) {
library.addIconPacks(fas)
}
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index d038aa5..904cda3 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -9,12 +9,15 @@ import {APIv3Interceptor} from "./api/interceptors/apiv3.interceptor";
import {IMAGE_LOADER} from "@angular/common";
import {loadImage} from "./helpers/data-fetching";
import {ApiTokenInterceptor} from "./api/interceptors/api-token.interceptor";
+import {provideAnimations} from "@angular/platform-browser/animations";
+import {provideAnimationsAsync} from "@angular/platform-browser/animations/async";
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideClientHydration(),
provideHttpClient(withInterceptorsFromDi()),
+ provideAnimationsAsync(),
useInterceptor(ApiBaseInterceptor),
useInterceptor(APIv3Interceptor),
useInterceptor(ApiTokenInterceptor),
diff --git a/src/app/banners/banner-info.interface.ts b/src/app/banners/banner-info.interface.ts
new file mode 100644
index 0000000..d08855f
--- /dev/null
+++ b/src/app/banners/banner-info.interface.ts
@@ -0,0 +1,11 @@
+import {IconProp} from "@fortawesome/fontawesome-svg-core";
+
+export interface BannerInfo {
+ // Styling
+ color: string
+ icon: IconProp
+
+ // Text
+ title: string
+ text: string
+}
\ No newline at end of file
diff --git a/src/app/banners/banner.service.ts b/src/app/banners/banner.service.ts
new file mode 100644
index 0000000..eca1d52
--- /dev/null
+++ b/src/app/banners/banner.service.ts
@@ -0,0 +1,42 @@
+import {Injectable} from "@angular/core";
+import {BannerInfo} from "./banner-info.interface";
+
+@Injectable({providedIn: 'root'})
+export class BannerService {
+ banners: BannerInfo[] = []
+
+ add(banner: BannerInfo) {
+ this.banners.push(banner);
+ }
+
+ dismiss(id: number): void {
+ this.banners.splice(id, 1);
+ }
+
+ success(title: string, text: string) {
+ this.add({
+ color: 'bg-success',
+ icon: 'check-circle',
+ text: text,
+ title: title,
+ })
+ }
+
+ warn(title: string, text: string) {
+ this.add({
+ color: 'bg-warning',
+ icon: 'warning',
+ text: text,
+ title: title,
+ })
+ }
+
+ error(title: string, text: string) {
+ this.add({
+ color: 'dangerous',
+ icon: 'exclamation-circle',
+ text: text,
+ title: title,
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/app/banners/popup-banner-container.component.ts b/src/app/banners/popup-banner-container.component.ts
new file mode 100644
index 0000000..8d9670b
--- /dev/null
+++ b/src/app/banners/popup-banner-container.component.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+import {BannerService} from "./banner.service";
+import {PopupBannerComponent} from "./popup-banner.component";
+import {NgForOf} from "@angular/common";
+
+@Component({
+ selector: 'app-popup-banner-container',
+ standalone: true,
+ imports: [
+ PopupBannerComponent,
+ NgForOf
+ ],
+ template: `
+
+
+
+ `
+})
+export class PopupBannerContainerComponent {
+ constructor(protected bannerService: BannerService) {
+ }
+}
diff --git a/src/app/banners/popup-banner.component.ts b/src/app/banners/popup-banner.component.ts
new file mode 100644
index 0000000..f866a03
--- /dev/null
+++ b/src/app/banners/popup-banner.component.ts
@@ -0,0 +1,65 @@
+import {Component, HostBinding, Input} from '@angular/core';
+import {BannerInfo} from "./banner-info.interface";
+import {FaIconComponent} from "@fortawesome/angular-fontawesome";
+import {NgIf} from "@angular/common";
+import {BannerService} from "./banner.service";
+import {animate, animateChild, query, style, transition, trigger} from "@angular/animations";
+
+@Component({
+ selector: 'app-popup-banner',
+ standalone: true,
+ imports: [
+ FaIconComponent,
+ NgIf
+ ],
+ animations: [
+ trigger('expand', [
+ transition(':leave', [
+ query('@*', animateChild(), {optional: true}),
+ animate('300ms ease-in-out', style({
+ opacity: 0,
+ height: 0,
+ 'padding-top': 0,
+ 'padding-bottom': 0,
+ 'border-bottom': 0
+ })),
+ ]),
+ transition(':enter', [
+ style({opacity: 0, height: 0, 'padding-top': 0, 'padding-bottom': 0}),
+ animate('500ms ease-in-out', style({
+ opacity: 1,
+ height: '*',
+ 'padding-top': '*',
+ 'padding-bottom': '*'
+ })),
+ ]),
+ ]),
+ ],
+ template: `
+
+ @defer (on timer(50ms)) {
+
+
+
+
+
{{ info.title }}
+
+
{{ info.text }}
+
+ }
+ `,
+})
+export class PopupBannerComponent {
+ @HostBinding("@expand") parentAnimation = true;
+
+ @Input() id: number = -1;
+ @Input({required: true}) info: BannerInfo = null!;
+
+ constructor(private bannerService: BannerService) {}
+
+ dismiss() {
+ if(this.id == -1) return;
+ this.bannerService.dismiss(this.id);
+ }
+}
diff --git a/src/app/debug/debug.routes.ts b/src/app/debug/debug.routes.ts
index 8c75caa..dbceb71 100644
--- a/src/app/debug/debug.routes.ts
+++ b/src/app/debug/debug.routes.ts
@@ -11,6 +11,11 @@ const debugRoutes: Routes = [
path: 'form',
loadComponent: () => import('./pages/form-debug/form-debug.component').then(x => x.FormDebugComponent),
data: {title: "Example Form"},
+ },
+ {
+ path: 'notifications',
+ loadComponent: () => import('./pages/notifications/notifications.component').then(x => x.NotificationsComponent),
+ data: {title: "Notifications Debug"},
}
]
diff --git a/src/app/debug/pages/notifications/notifications.component.html b/src/app/debug/pages/notifications/notifications.component.html
new file mode 100644
index 0000000..5001ce3
--- /dev/null
+++ b/src/app/debug/pages/notifications/notifications.component.html
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/debug/pages/notifications/notifications.component.ts b/src/app/debug/pages/notifications/notifications.component.ts
new file mode 100644
index 0000000..7fa3112
--- /dev/null
+++ b/src/app/debug/pages/notifications/notifications.component.ts
@@ -0,0 +1,27 @@
+import { Component } from '@angular/core';
+import {PageTitleComponent} from "../../../components/ui/text/page-title.component";
+import {ButtonGroupComponent} from "../../../components/ui/form/button-group.component";
+import {ButtonComponent} from "../../../components/ui/form/button.component";
+import {faCheckCircle} from "@fortawesome/free-solid-svg-icons";
+import {BannerService} from "../../../banners/banner.service";
+
+@Component({
+ selector: 'app-notifications',
+ standalone: true,
+ imports: [
+ PageTitleComponent,
+ ButtonGroupComponent,
+ ButtonComponent
+ ],
+ templateUrl: './notifications.component.html'
+})
+export class NotificationsComponent {
+ protected readonly faCheckCircle = faCheckCircle;
+
+ constructor(private bannerService: BannerService) {
+ }
+
+ bannerSuccess() {
+ this.bannerService.success("You Did It", "Congration On Doing The Thing");
+ }
+}
diff --git a/src/app/pages/auth/login/login.component.html b/src/app/pages/auth/login/login.component.html
index cd46989..b503425 100644
--- a/src/app/pages/auth/login/login.component.html
+++ b/src/app/pages/auth/login/login.component.html
@@ -2,7 +2,7 @@
-
+