generated from nhcarrigan/template
feat: multiple improvements to library functionality #50
@@ -2,6 +2,7 @@ import {
|
||||
ApplicationConfig,
|
||||
provideBrowserGlobalErrorListeners,
|
||||
APP_INITIALIZER,
|
||||
ErrorHandler,
|
||||
} from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient, withInterceptors, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
@@ -9,12 +10,17 @@ import { appRoutes } from './app.routes';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||
import { initializeAuth } from './initializers/auth.initializer';
|
||||
import { GlobalErrorHandler } from './services/global-error-handler.service';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideRouter(appRoutes),
|
||||
provideHttpClient(),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: GlobalErrorHandler
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AuthInterceptor,
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
<app-footer></app-footer>
|
||||
<app-toast></app-toast>
|
||||
|
||||
@@ -2,10 +2,11 @@ import { Component, inject, OnInit } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { ToastComponent } from './components/toast/toast.component';
|
||||
import { AnalyticsService } from './services/analytics.service';
|
||||
|
||||
@Component({
|
||||
imports: [RouterModule, HeaderComponent, FooterComponent],
|
||||
imports: [RouterModule, HeaderComponent, FooterComponent, ToastComponent],
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss',
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
border: 2px solid;
|
||||
background-color: var(--witch-purple);
|
||||
color: var(--moon-white);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-color: #ff4444;
|
||||
background-color: rgba(255, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-color: #44ff88;
|
||||
background-color: rgba(68, 255, 136, 0.4);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-color: var(--witch-lavender);
|
||||
background-color: rgba(200, 162, 200, 0.4);
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-color: #ffaa44;
|
||||
background-color: rgba(255, 170, 68, 0.4);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--moon-white);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
@copyright 2026 NHCarrigan
|
||||
@license Naomi's Public License
|
||||
@author Naomi Carrigan
|
||||
-->
|
||||
|
||||
<div class="toast-container">
|
||||
@for (toast of toastService.toastList(); track toast.id) {
|
||||
<div class="toast toast-{{ toast.type }}" (click)="toastService.remove(toast.id)">
|
||||
<div class="toast-icon">
|
||||
@switch (toast.type) {
|
||||
@case ('error') { ❌ }
|
||||
@case ('success') { âś… }
|
||||
@case ('info') { ℹ️ }
|
||||
@case ('warning') { ⚠️ }
|
||||
}
|
||||
</div>
|
||||
<div class="toast-message">{{ toast.message }}</div>
|
||||
<button class="toast-close" (click)="toastService.remove(toast.id)">Ă—</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ToastService } from '../../services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toast',
|
||||
standalone: true,
|
||||
templateUrl: './toast.component.html',
|
||||
styleUrls: ['./toast.component.css']
|
||||
})
|
||||
export class ToastComponent {
|
||||
public toastService = inject(ToastService);
|
||||
}
|
||||
@@ -16,11 +16,13 @@ import {
|
||||
import { Observable, catchError, throwError, switchMap, BehaviorSubject, filter, take } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { ToastService } from '../services/toast.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
private router = inject(Router);
|
||||
private http = inject(HttpClient);
|
||||
private toast = inject(ToastService);
|
||||
|
||||
private isRefreshing = false;
|
||||
private refreshTokenSubject = new BehaviorSubject<boolean | null>(null);
|
||||
@@ -36,6 +38,9 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
if (error.status === 401 && !request.url.includes('/auth/refresh') && !request.url.includes('/auth/logout')) {
|
||||
return this.handle401Error(authReq, next);
|
||||
}
|
||||
|
||||
// Show toast for other HTTP errors
|
||||
this.showErrorToast(error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
@@ -59,6 +64,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
catchError((err) => {
|
||||
this.isRefreshing = false;
|
||||
this.refreshTokenSubject.next(false);
|
||||
this.toast.error('Your session has expired. Please log in again.');
|
||||
this.router.navigate(['/']);
|
||||
return throwError(() => err);
|
||||
})
|
||||
@@ -76,4 +82,28 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private showErrorToast(error: HttpErrorResponse): void {
|
||||
let message = 'Something went wrong. Please try again.';
|
||||
|
||||
switch (error.status) {
|
||||
case 400:
|
||||
message = error.error?.message || 'Invalid request. Please check your input.';
|
||||
break;
|
||||
case 403:
|
||||
message = 'You do not have permission to perform this action.';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Resource not found.';
|
||||
break;
|
||||
case 500:
|
||||
message = 'Server error. Please try again later.';
|
||||
break;
|
||||
case 0:
|
||||
message = 'Network error. Please check your connection.';
|
||||
break;
|
||||
}
|
||||
|
||||
this.toast.error(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { ErrorHandler, Injectable, inject } from '@angular/core';
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
private toast = inject(ToastService);
|
||||
|
||||
handleError(error: Error): void {
|
||||
console.error('Global error caught:', error);
|
||||
|
||||
// Show user-friendly error message
|
||||
const message = this.getUserFriendlyMessage(error);
|
||||
this.toast.error(message);
|
||||
}
|
||||
|
||||
private getUserFriendlyMessage(error: Error): string {
|
||||
// Check for common error types
|
||||
if (error.message.includes('Http failure')) {
|
||||
return 'Network error. Please check your connection.';
|
||||
}
|
||||
|
||||
if (error.message.includes('401') || error.message.includes('403')) {
|
||||
return 'Your session has expired. Please refresh the page.';
|
||||
}
|
||||
|
||||
if (error.message.includes('404')) {
|
||||
return 'Resource not found.';
|
||||
}
|
||||
|
||||
if (error.message.includes('500')) {
|
||||
return 'Server error. Please try again later.';
|
||||
}
|
||||
|
||||
// Generic error message
|
||||
return 'Something went wrong. Please try again.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
type: 'error' | 'success' | 'info' | 'warning';
|
||||
duration: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ToastService {
|
||||
private toasts = signal<Toast[]>([]);
|
||||
public readonly toastList = this.toasts.asReadonly();
|
||||
private nextId = 0;
|
||||
|
||||
/**
|
||||
* Show an error toast notification.
|
||||
*/
|
||||
error(message: string, duration = 5000): void {
|
||||
this.addToast(message, 'error', duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success toast notification.
|
||||
*/
|
||||
success(message: string, duration = 3000): void {
|
||||
this.addToast(message, 'success', duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an info toast notification.
|
||||
*/
|
||||
info(message: string, duration = 3000): void {
|
||||
this.addToast(message, 'info', duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning toast notification.
|
||||
*/
|
||||
warning(message: string, duration = 4000): void {
|
||||
this.addToast(message, 'warning', duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a toast by ID.
|
||||
*/
|
||||
remove(id: number): void {
|
||||
this.toasts.update(toasts => toasts.filter(t => t.id !== id));
|
||||
}
|
||||
|
||||
private addToast(message: string, type: Toast['type'], duration: number): void {
|
||||
const id = this.nextId++;
|
||||
const toast: Toast = { id, message, type, duration };
|
||||
|
||||
this.toasts.update(toasts => [...toasts, toast]);
|
||||
|
||||
// Auto-remove after duration
|
||||
setTimeout(() => {
|
||||
this.remove(id);
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user