diff --git a/frontend/Exence/src/app/shared/snackbar/snackbar.component.html b/frontend/Exence/src/app/shared/snackbar/snackbar.component.html index 60ce243..94d946a 100644 --- a/frontend/Exence/src/app/shared/snackbar/snackbar.component.html +++ b/frontend/Exence/src/app/shared/snackbar/snackbar.component.html @@ -1,23 +1,26 @@ -
-
+
+
@switch (data.type) { @case (errorType) { - error + error } @case (warnType) { - warning + warning } @case (infoType) { - info + info } @case (successType) { - check + check } }
@if (data.action) { @@ -39,8 +42,16 @@ mat-icon-button matSnackBarAction (click)="onClose()" + class="close-button" > close
+ +
+
+
diff --git a/frontend/Exence/src/app/shared/snackbar/snackbar.component.scss b/frontend/Exence/src/app/shared/snackbar/snackbar.component.scss index 6d56793..c259182 100644 --- a/frontend/Exence/src/app/shared/snackbar/snackbar.component.scss +++ b/frontend/Exence/src/app/shared/snackbar/snackbar.component.scss @@ -4,12 +4,31 @@ --mat-icon-button-icon-color: currentColor; } +section { + border-radius: 5px; + background-color: lighten(var(--app-card-color), 10%); + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + + transition: opacity 300ms ease-out, box-shadow 300ms ease-out; + opacity: 1; + + overflow: hidden; + + &.fade-out { + opacity: 0; + box-shadow: unset; + } +} + .notification { &-error { color: var(--error-color); } - &-success { - color: var(--primary-color); + &-success, &-info { + color: var(--default-text-color); + .close-button, .type-icon { + color: var(--primary-color); + } } &-warning { color: var(--warn-color); @@ -18,4 +37,13 @@ mat-icon { flex-shrink: 0; +} + +.progressbar { + height: 2px; + + div { + transition: width 0.1s; + background-color: var(--primary-color); + } } \ No newline at end of file diff --git a/frontend/Exence/src/app/shared/snackbar/snackbar.component.ts b/frontend/Exence/src/app/shared/snackbar/snackbar.component.ts index 03f363e..8a1b402 100644 --- a/frontend/Exence/src/app/shared/snackbar/snackbar.component.ts +++ b/frontend/Exence/src/app/shared/snackbar/snackbar.component.ts @@ -1,10 +1,10 @@ -import { Component, inject } from '@angular/core'; -import { MAT_SNACK_BAR_DATA, MatSnackBarRef } from '@angular/material/snack-bar'; -import { SnackbarType } from './snackbar.service'; -import { SvgIcons } from '../svg-icons/svg-icons'; +import { Component, DestroyRef, inject, signal } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; +import { MAT_SNACK_BAR_DATA, MatSnackBarRef } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatButtonModule } from '@angular/material/button'; +import { SvgIcons } from '../svg-icons/svg-icons'; +import { SnackbarType } from './snackbar.service'; export interface SnackbarData { message: string; @@ -17,6 +17,8 @@ export interface SnackbarData { }; } +export const SNACKBAR_DISMISS_DURATION = 5000; + @Component({ selector: 'ex-snackbar', templateUrl: './snackbar.component.html', @@ -29,13 +31,28 @@ export interface SnackbarData { }) export class SnackbarComponent { private readonly snackbarRef = inject(MatSnackBarRef); + private readonly destroyRef = inject(DestroyRef); readonly data = inject(MAT_SNACK_BAR_DATA); + private animationFrameId?: number; + readonly errorType = SnackbarType.Error; readonly warnType = SnackbarType.Warning; readonly infoType = SnackbarType.Info; readonly successType = SnackbarType.Success; + progress = signal(0); + fadeOutStarted = signal(false); + + constructor() { + this.startProgressAnimation(); + this.destroyRef.onDestroy(() => { + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + } + }); + } + getCssClass(): string { switch (this.data.type) { case SnackbarType.Error: @@ -52,6 +69,30 @@ export class SnackbarComponent { onClose(): void { this.snackbarRef.dismiss(); } + + private startProgressAnimation(): void { + const startTime = performance.now(); + + const animate = (currentTime: number): void => { + if (!startTime) return; + + const elapsed = currentTime - startTime; + const progressValue = Math.min(elapsed, SNACKBAR_DISMISS_DURATION); + const newProgress = Math.floor((progressValue / SNACKBAR_DISMISS_DURATION) * 100); + + this.progress.set(newProgress); + + if (progressValue >= SNACKBAR_DISMISS_DURATION - 300) { + this.fadeOutStarted.set(true); + } + + if (progressValue < SNACKBAR_DISMISS_DURATION) { + this.animationFrameId = requestAnimationFrame(animate); + } + }; + + this.animationFrameId = requestAnimationFrame(animate); + } } diff --git a/frontend/Exence/src/app/shared/snackbar/snackbar.service.ts b/frontend/Exence/src/app/shared/snackbar/snackbar.service.ts index 818306b..6165e4e 100644 --- a/frontend/Exence/src/app/shared/snackbar/snackbar.service.ts +++ b/frontend/Exence/src/app/shared/snackbar/snackbar.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { MatSnackBar, MatSnackBarConfig, SimpleSnackBar } from '@angular/material/snack-bar'; -import { SnackbarComponent, SnackbarData } from './snackbar.component'; +import { SNACKBAR_DISMISS_DURATION, SnackbarComponent, SnackbarData } from './snackbar.component'; export const enum SnackbarType { Error, @@ -16,11 +16,18 @@ export class SnackbarService { private readonly snackbar = inject(MatSnackBar); private readonly snackbarConfig: MatSnackBarConfig = { - panelClass: 'custom-snackbar' + panelClass: 'custom-snackbar', + duration: SNACKBAR_DISMISS_DURATION, + horizontalPosition: 'right', + verticalPosition: 'top', }; showCustom(data: SnackbarData): void { - this.snackbar.openFromComponent(SnackbarComponent, { ...this.snackbarConfig, data }); + this.snackbar.openFromComponent(SnackbarComponent, { + ...this.snackbarConfig, + data, + duration: SNACKBAR_DISMISS_DURATION, + }); } showError(message: string): void { @@ -29,7 +36,7 @@ export class SnackbarService { data: { message, type: SnackbarType.Error - } satisfies SnackbarData + } satisfies SnackbarData, }); } @@ -39,29 +46,27 @@ export class SnackbarService { data: { message, type: SnackbarType.Warning - } satisfies SnackbarData + } satisfies SnackbarData, }); } showInfo(message: string): void { this.snackbar.openFromComponent(SnackbarComponent, { ...this.snackbarConfig, - duration: 10000, data: { message, type: SnackbarType.Info - } satisfies SnackbarData + } satisfies SnackbarData, }); } showSuccess(message: string): void { this.snackbar.openFromComponent(SnackbarComponent, { ...this.snackbarConfig, - duration: 10000, data: { message, type: SnackbarType.Success - } satisfies SnackbarData + } satisfies SnackbarData, }); } } \ No newline at end of file diff --git a/frontend/Exence/src/styles/components/snackbar.scss b/frontend/Exence/src/styles/components/snackbar.scss index f295447..cef2a92 100644 --- a/frontend/Exence/src/styles/components/snackbar.scss +++ b/frontend/Exence/src/styles/components/snackbar.scss @@ -1,3 +1,5 @@ +@import "../imports.scss"; + mat-snack-bar-container.mat-mdc-snack-bar-container.custom-snackbar { .mdc-snackbar__label { --mat-snack-bar-supporting-text-size: 16px; @@ -5,6 +7,12 @@ mat-snack-bar-container.mat-mdc-snack-bar-container.custom-snackbar { } .mat-mdc-snackbar-surface { - --mat-snack-bar-container-color: var(--app-card-color); - } + background-color: unset; + box-shadow: unset; + padding: 0; + + .mat-mdc-snack-bar-label { + padding: 0; + } + } } \ No newline at end of file diff --git a/frontend/Exence/src/styles/styles.scss b/frontend/Exence/src/styles/styles.scss index 5aedb74..a8024bd 100644 --- a/frontend/Exence/src/styles/styles.scss +++ b/frontend/Exence/src/styles/styles.scss @@ -67,7 +67,7 @@ a { } hr { - background-color: lighten(--secondary-color, 45%); + background-color: lighten(var(--secondary-color), 45%); height: 1px; border: none; opacity: 0.36; @@ -122,4 +122,4 @@ hr { mat-card.mat-mdc-card.categories mat-card-header .mat-mdc-card-header-text { width: 100%; -} +} \ No newline at end of file