/** * Professional Navigation Service * Enterprise-grade navigation handling with error management and user feedback * * @author David Valera Melendez * @created 2025-08-08 * @location Made in Germany 🇩🇪 */ import { Injectable } from '@angular/core'; import { Router, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router'; import { Location } from '@angular/common'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Observable, BehaviorSubject } from 'rxjs'; import { filter } from 'rxjs/operators'; export interface NavigationState { isNavigating: boolean; currentUrl: string; previousUrl: string | null; error: string | null; } @Injectable({ providedIn: 'root' }) export class NavigationService { private navigationStateSubject = new BehaviorSubject({ isNavigating: false, currentUrl: '/', previousUrl: null, error: null }); public readonly navigationState$: Observable = this.navigationStateSubject.asObservable(); constructor( private router: Router, private location: Location, private snackBar: MatSnackBar ) { this.initializeNavigationTracking(); } /** * Initialize navigation event tracking */ private initializeNavigationTracking(): void { // Track navigation start this.router.events.subscribe(event => { if (event.constructor.name === 'NavigationStart') { this.updateNavigationState({ ...this.navigationStateSubject.value, isNavigating: true, error: null }); } }); // Track successful navigation this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { this.updateNavigationState({ previousUrl: this.navigationStateSubject.value.currentUrl, currentUrl: event.url, isNavigating: false, error: null }); } }); // Track navigation errors this.router.events.subscribe(event => { if (event instanceof NavigationError) { console.error('Navigation error:', event.error); this.updateNavigationState({ ...this.navigationStateSubject.value, isNavigating: false, error: event.error?.message || 'Navigation failed' }); this.showErrorMessage('Navigation failed. Please try again.'); } }); // Track navigation cancellations this.router.events.subscribe(event => { if (event instanceof NavigationCancel) { console.warn('Navigation cancelled:', event.reason); this.updateNavigationState({ ...this.navigationStateSubject.value, isNavigating: false, error: null }); } }); } /** * Update navigation state */ private updateNavigationState(state: NavigationState): void { this.navigationStateSubject.next(state); } /** * Navigate to a route with comprehensive error handling */ async navigateToRoute(route: string | string[], options?: { replaceUrl?: boolean; queryParams?: any; fragment?: string; state?: any; skipLocationChange?: boolean; preserveFragment?: boolean; preserveQueryParams?: boolean; }): Promise { try { const navigationResult = await this.router.navigate( Array.isArray(route) ? route : [route], { replaceUrl: options?.replaceUrl || false, queryParams: options?.queryParams, fragment: options?.fragment, state: options?.state, skipLocationChange: options?.skipLocationChange || false, preserveFragment: options?.preserveFragment || false, queryParamsHandling: options?.preserveQueryParams ? 'merge' : undefined } ); if (!navigationResult) { console.error('Navigation failed for route:', route); this.showErrorMessage('Unable to navigate to the requested page.'); return false; } return true; } catch (error) { console.error('Navigation exception:', error); this.showErrorMessage('An unexpected error occurred during navigation.'); return false; } } /** * Navigate to login page with return URL */ async navigateToLogin(returnUrl?: string): Promise { const queryParams = returnUrl ? { returnUrl } : undefined; return await this.navigateToRoute('/auth/login', { queryParams }); } /** * Navigate to dashboard */ async navigateToDashboard(): Promise { return await this.navigateToRoute('/dashboard'); } /** * Navigate to resume builder */ async navigateToBuilder(): Promise { return await this.navigateToRoute('/builder'); } /** * Navigate to user profile */ async navigateToProfile(): Promise { return await this.navigateToRoute('/profile'); } /** * Navigate to settings */ async navigateToSettings(): Promise { return await this.navigateToRoute('/settings'); } /** * Navigate to home page */ async navigateToHome(): Promise { return await this.navigateToRoute('/home', { replaceUrl: true }); } /** * Navigate back to previous page */ navigateBack(): void { const previousUrl = this.navigationStateSubject.value.previousUrl; if (previousUrl && previousUrl !== this.navigationStateSubject.value.currentUrl) { this.location.back(); } else { this.navigateToHome(); } } /** * Navigate forward in browser history */ navigateForward(): void { this.location.forward(); } /** * Get current URL */ getCurrentUrl(): string { return this.router.url; } /** * Check if currently on a specific route */ isCurrentRoute(route: string): boolean { return this.router.url === route || this.router.url.startsWith(route + '/'); } /** * Check if route is public (doesn't require authentication) */ isPublicRoute(url?: string): boolean { const currentUrl = url || this.router.url; const publicRoutes = ['/home', '/login', '/register', '/forgot-password', '/auth', '/404', '/403', '/500']; return publicRoutes.some(route => currentUrl.startsWith(route)) || currentUrl === '/'; } /** * Force reload current page */ reloadCurrentPage(): void { const currentUrl = this.router.url; this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { this.router.navigate([currentUrl]); }); } /** * Open external URL in new tab */ openExternalUrl(url: string): void { window.open(url, '_blank', 'noopener,noreferrer'); } /** * Show success message to user */ private showSuccessMessage(message: string): void { this.snackBar.open(message, 'Close', { duration: 4000, panelClass: ['success-snackbar'], horizontalPosition: 'end', verticalPosition: 'top' }); } /** * Show error message to user */ private showErrorMessage(message: string): void { this.snackBar.open(message, 'Close', { duration: 6000, panelClass: ['error-snackbar'], horizontalPosition: 'end', verticalPosition: 'top' }); } /** * Show info message to user */ private showInfoMessage(message: string): void { this.snackBar.open(message, 'Close', { duration: 3000, panelClass: ['info-snackbar'], horizontalPosition: 'end', verticalPosition: 'top' }); } }