diff --git a/package-lock.json b/package-lock.json index a8ad1ad..9420051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@angular/router": "^18.2.7", "@dhutaryan/ngx-mat-timepicker": "^18.0.2", "@progress/kendo-date-math": "^1.5.13", + "ngx-toastr": "^19.0.0", "rxjs": "~7.8.1", "tslib": "^2.7.0", "zone.js": "^0.14.10" @@ -9904,6 +9905,20 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ngx-toastr": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz", + "integrity": "sha512-6pTnktwwWD+kx342wuMOWB4+bkyX9221pAgGz3SHOJH0/MI9erLucS8PeeJDFwbUYyh75nQ6AzVtolgHxi52dQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0-0", + "@angular/core": ">=16.0.0-0", + "@angular/platform-browser": ">=16.0.0-0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", diff --git a/package.json b/package.json index d53a786..d675385 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@angular/router": "^18.2.7", "@dhutaryan/ngx-mat-timepicker": "^18.0.2", "@progress/kendo-date-math": "^1.5.13", + "ngx-toastr": "^19.0.0", "rxjs": "~7.8.1", "tslib": "^2.7.0", "zone.js": "^0.14.10" diff --git a/src/api/api.service.ts b/src/api/api.service.ts index d0933d8..6ff2f48 100644 --- a/src/api/api.service.ts +++ b/src/api/api.service.ts @@ -1,12 +1,12 @@ import {catchError, distinctUntilChanged, filter, first, mergeMap, Observable, retryWhen, switchMap, timer} from "rxjs"; import {HttpClient, HttpErrorResponse} from "@angular/common/http"; -import {NotifyColor, OpenNotifyService} from "@service/open-notify.service"; import {environment} from "@environment"; import {Router} from "@angular/router"; import {Injectable} from "@angular/core"; import {RequestBuilder, RequestData} from "@api/RequestBuilder"; import {TokenRefreshService} from "@service/token-refresh.service"; import {AuthToken} from "@service/auth.service"; +import {ToastrService} from "ngx-toastr"; export function retryWithInterval(): (source: Observable) => Observable { return (source: Observable) => @@ -37,7 +37,7 @@ export enum AvailableVersion { @Injectable() export default abstract class ApiService { - constructor(private http: HttpClient, private notify: OpenNotifyService, private router: Router, protected tokenRefreshService: TokenRefreshService) { + constructor(private http: HttpClient, private notify: ToastrService, private router: Router, protected tokenRefreshService: TokenRefreshService) { } private apiUrl = environment.apiUrl; @@ -154,6 +154,7 @@ export default abstract class ApiService { } let message: string; + let description: string | undefined = undefined; if (error.error instanceof ErrorEvent) { message = `Произошла ошибка: ${error.error.message}`; } else { @@ -185,9 +186,9 @@ export default abstract class ApiService { break; } if (error.error?.Error) { - message += ` ${error.error.Error}`; + description = `${error.error.Error}`; } } - this.notify.open(message, NotifyColor.Danger); + this.notify.error(description, message); } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index c142778..37cc868 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -4,7 +4,23 @@ import {provideRouter} from '@angular/router'; import {routes} from './app.routes'; import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; import {provideHttpClient} from "@angular/common/http"; +import {provideToastr} from "ngx-toastr"; export const appConfig: ApplicationConfig = { - providers: [provideRouter(routes), provideAnimationsAsync(), provideHttpClient()] + providers: [ + provideRouter(routes), + provideAnimationsAsync(), + provideHttpClient(), + provideToastr({ + timeOut: 5000, + extendedTimeOut: 2000, + positionClass: "toast-top-right", + progressBar: true, + progressAnimation: "decreasing", + newestOnTop: true, + tapToDismiss: true, + disableTimeOut: false, + autoDismiss: true, + maxOpened: 5 + })] }; diff --git a/src/services/open-notify.service.ts b/src/services/open-notify.service.ts deleted file mode 100644 index c3bad01..0000000 --- a/src/services/open-notify.service.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {Injectable} from '@angular/core'; -import {MatSnackBar} from '@angular/material/snack-bar'; -import {NotificationComponent} from "@component/common/notification/notification.component"; - -export enum NotifyColor { - Basic, - Warn, - Danger, - Success -} - -@Injectable({ - providedIn: 'root' -}) - -export class OpenNotifyService { - constructor(private _snackBar: MatSnackBar) { - } - - colorClass: string = ''; - - set setColorClass(color: NotifyColor) { - switch (color) { - case NotifyColor.Warn: { - this.colorClass = 'yellow-snackbar'; - } - break; - case NotifyColor.Danger: { - this.colorClass = 'red-snackbar'; - } - break; - case NotifyColor.Success: { - this.colorClass = 'green-snackbar'; - } - break; - default: { - this.colorClass = 'snackbar'; - } - break; - } - } - - open(message: string, color: NotifyColor = NotifyColor.Basic, duration: number = 5000) { - this.setColorClass = color; - this._snackBar.openFromComponent(NotificationComponent, { - data: { - message: message, - duration: duration, - className: this.colorClass, - color: color === NotifyColor.Danger ? "accent" : "primary" - }, - duration: duration, - verticalPosition: 'top', - horizontalPosition: 'center', - panelClass: [this.colorClass] - }) - ; - } -} diff --git a/src/styles.css b/src/styles.css index 2e52d46..b428377 100644 --- a/src/styles.css +++ b/src/styles.css @@ -10,22 +10,194 @@ body { font-family: Roboto, "Helvetica Neue", sans-serif; } -.green-snackbar { - --mdc-snackbar-container-color: #8CBA51; - color: black; +/* based on angular-toastr css https://github.com/Foxandxss/angular-toastr/blob/cb508fe6801d6b288d3afc525bb40fee1b101650/dist/angular-toastr.css */ +/* position */ +.toast-center-center { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +.toast-top-center { + top: 0; + right: 0; + width: 100%; +} +.toast-bottom-center { + bottom: 0; + right: 0; + width: 100%; +} +.toast-top-full-width { + top: 0; + right: 0; + width: 100%; +} +.toast-bottom-full-width { + bottom: 0; + right: 0; + width: 100%; +} +.toast-top-left { + top: 12px; + left: 12px; +} +.toast-top-right { + top: 12px; + right: 12px; +} +.toast-bottom-right { + right: 12px; + bottom: 12px; +} +.toast-bottom-left { + bottom: 12px; + left: 12px; } -.red-snackbar { - --mdc-snackbar-container-color: #E20338; - color: white; +/* toast styles */ +.toast-title { + font-weight: bold; } - -.yellow-snackbar { - --mdc-snackbar-container-color: #FFD600; - color: black; +.toast-message { + word-wrap: break-word; } - -.snackbar { - --mdc-snackbar-container-color: inherit; - color: inherit; +.toast-message a, +.toast-message label { + color: #FFFFFF; +} +.toast-message a:hover { + color: #CCCCCC; + text-decoration: none; +} +.toast-close-button { + position: relative; + right: -0.3em; + top: -0.3em; + float: right; + font-size: 20px; + font-weight: bold; + color: #FFFFFF; + text-shadow: 0 1px 0 #ffffff; + /* opacity: 0.8; */ +} +.toast-close-button:hover, +.toast-close-button:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; +} +/*Additional properties for button version + iOS requires the button element instead of an anchor tag. + If you want the anchor version, it requires `href="#"`.*/ +button.toast-close-button { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.toast-container { + pointer-events: none; + position: fixed; + z-index: 999999; +} +.toast-container * { + box-sizing: border-box; +} +.toast-container .ngx-toastr { + position: relative; + overflow: hidden; + margin: 0 0 6px; + padding: 15px 15px 15px 50px; + width: 300px; + border-radius: 3px 3px 3px 3px; + background-position: 15px center; + background-repeat: no-repeat; + background-size: 24px; + box-shadow: 0 0 12px #999999; + color: #FFFFFF; +} +.toast-container .ngx-toastr:hover { + box-shadow: 0 0 12px #000000; + opacity: 1; + cursor: pointer; +} +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/info-circle.svg */ +.toast-info { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOS4wNDMgOCA4IDExOS4wODMgOCAyNTZjMCAxMzYuOTk3IDExMS4wNDMgMjQ4IDI0OCAyNDhzMjQ4LTExMS4wMDMgMjQ4LTI0OEM1MDQgMTE5LjA4MyAzOTIuOTU3IDggMjU2IDh6bTAgMTEwYzIzLjE5NiAwIDQyIDE4LjgwNCA0MiA0MnMtMTguODA0IDQyLTQyIDQyLTQyLTE4LjgwNC00Mi00MiAxOC44MDQtNDIgNDItNDJ6bTU2IDI1NGMwIDYuNjI3LTUuMzczIDEyLTEyIDEyaC04OGMtNi42MjcgMC0xMi01LjM3My0xMi0xMnYtMjRjMC02LjYyNyA1LjM3My0xMiAxMi0xMmgxMnYtNjRoLTEyYy02LjYyNyAwLTEyLTUuMzczLTEyLTEydi0yNGMwLTYuNjI3IDUuMzczLTEyIDEyLTEyaDY0YzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MTAwaDEyYzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MjR6Jy8+PC9zdmc+"); +} +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/times-circle.svg */ +.toast-error { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOSA4IDggMTE5IDggMjU2czExMSAyNDggMjQ4IDI0OCAyNDgtMTExIDI0OC0yNDhTMzkzIDggMjU2IDh6bTEyMS42IDMxMy4xYzQuNyA0LjcgNC43IDEyLjMgMCAxN0wzMzggMzc3LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwyNTYgMzEybC02NS4xIDY1LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwxMzQuNCAzMzhjLTQuNy00LjctNC43LTEyLjMgMC0xN2w2NS42LTY1LTY1LjYtNjUuMWMtNC43LTQuNy00LjctMTIuMyAwLTE3bDM5LjYtMzkuNmM0LjctNC43IDEyLjMtNC43IDE3IDBsNjUgNjUuNyA2NS4xLTY1LjZjNC43LTQuNyAxMi4zLTQuNyAxNyAwbDM5LjYgMzkuNmM0LjcgNC43IDQuNyAxMi4zIDAgMTdMMzEyIDI1Nmw2NS42IDY1LjF6Jy8+PC9zdmc+"); +} +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/check.svg */ +.toast-success { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTE3My44OTggNDM5LjQwNGwtMTY2LjQtMTY2LjRjLTkuOTk3LTkuOTk3LTkuOTk3LTI2LjIwNiAwLTM2LjIwNGwzNi4yMDMtMzYuMjA0YzkuOTk3LTkuOTk4IDI2LjIwNy05Ljk5OCAzNi4yMDQgMEwxOTIgMzEyLjY5IDQzMi4wOTUgNzIuNTk2YzkuOTk3LTkuOTk3IDI2LjIwNy05Ljk5NyAzNi4yMDQgMGwzNi4yMDMgMzYuMjA0YzkuOTk3IDkuOTk3IDkuOTk3IDI2LjIwNiAwIDM2LjIwNGwtMjk0LjQgMjk0LjQwMWMtOS45OTggOS45OTctMjYuMjA3IDkuOTk3LTM2LjIwNC0uMDAxeicvPjwvc3ZnPg=="); +} +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/exclamation-triangle.svg */ +.toast-warning { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1NzYgNTEyJyB3aWR0aD0nNTc2JyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTU2OS41MTcgNDQwLjAxM0M1ODcuOTc1IDQ3Mi4wMDcgNTY0LjgwNiA1MTIgNTI3Ljk0IDUxMkg0OC4wNTRjLTM2LjkzNyAwLTU5Ljk5OS00MC4wNTUtNDEuNTc3LTcxLjk4N0wyNDYuNDIzIDIzLjk4NWMxOC40NjctMzIuMDA5IDY0LjcyLTMxLjk1MSA4My4xNTQgMGwyMzkuOTQgNDE2LjAyOHpNMjg4IDM1NGMtMjUuNDA1IDAtNDYgMjAuNTk1LTQ2IDQ2czIwLjU5NSA0NiA0NiA0NiA0Ni0yMC41OTUgNDYtNDYtMjAuNTk1LTQ2LTQ2LTQ2em0tNDMuNjczLTE2NS4zNDZsNy40MTggMTM2Yy4zNDcgNi4zNjQgNS42MDkgMTEuMzQ2IDExLjk4MiAxMS4zNDZoNDguNTQ2YzYuMzczIDAgMTEuNjM1LTQuOTgyIDExLjk4Mi0xMS4zNDZsNy40MTgtMTM2Yy4zNzUtNi44NzQtNS4wOTgtMTIuNjU0LTExLjk4Mi0xMi42NTRoLTYzLjM4M2MtNi44ODQgMC0xMi4zNTYgNS43OC0xMS45ODEgMTIuNjU0eicvPjwvc3ZnPg=="); +} +.toast-container.toast-top-center .ngx-toastr, +.toast-container.toast-bottom-center .ngx-toastr { + width: 300px; + margin-left: auto; + margin-right: auto; +} +.toast-container.toast-top-full-width .ngx-toastr, +.toast-container.toast-bottom-full-width .ngx-toastr { + width: 96%; + margin-left: auto; + margin-right: auto; +} +.ngx-toastr { + background-color: #030303; + pointer-events: auto; +} +.toast-success { + background-color: #51A351; +} +.toast-error { + background-color: #BD362F; +} +.toast-info { + background-color: #2F96B4; +} +.toast-warning { + background-color: #F89406; +} +.toast-progress { + position: absolute; + left: 0; + bottom: 0; + height: 4px; + background-color: #000000; + opacity: 0.4; +} +/* Responsive Design */ +@media all and (max-width: 240px) { + .toast-container .ngx-toastr.div { + padding: 8px 8px 8px 50px; + width: 11em; + } + .toast-container .toast-close-button { + right: -0.2em; + top: -0.2em; + } +} +@media all and (min-width: 241px) and (max-width: 480px) { + .toast-container .ngx-toastr.div { + padding: 8px 8px 8px 50px; + width: 18em; + } + .toast-container .toast-close-button { + right: -0.2em; + top: -0.2em; + } +} +@media all and (min-width: 481px) and (max-width: 768px) { + .toast-container .ngx-toastr.div { + padding: 15px 15px 15px 50px; + width: 25em; + } }