init commit
This commit is contained in:
282
src/app/core/services/navigation.service.ts
Normal file
282
src/app/core/services/navigation.service.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Professional Navigation Service
|
||||
* Enterprise-grade navigation handling with error management and user feedback
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @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<NavigationState>({
|
||||
isNavigating: false,
|
||||
currentUrl: '/',
|
||||
previousUrl: null,
|
||||
error: null
|
||||
});
|
||||
|
||||
public readonly navigationState$: Observable<NavigationState> = 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<boolean> {
|
||||
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<boolean> {
|
||||
const queryParams = returnUrl ? { returnUrl } : undefined;
|
||||
return await this.navigateToRoute('/auth/login', { queryParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to dashboard
|
||||
*/
|
||||
async navigateToDashboard(): Promise<boolean> {
|
||||
return await this.navigateToRoute('/dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to resume builder
|
||||
*/
|
||||
async navigateToBuilder(): Promise<boolean> {
|
||||
return await this.navigateToRoute('/builder');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to user profile
|
||||
*/
|
||||
async navigateToProfile(): Promise<boolean> {
|
||||
return await this.navigateToRoute('/profile');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to settings
|
||||
*/
|
||||
async navigateToSettings(): Promise<boolean> {
|
||||
return await this.navigateToRoute('/settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to home page
|
||||
*/
|
||||
async navigateToHome(): Promise<boolean> {
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user