veza/apps/web/src/components/ui/LazyComponent.tsx

410 lines
12 KiB
TypeScript
Raw Normal View History

import {
Suspense,
lazy,
type ComponentType,
Component,
type ErrorInfo,
} from 'react';
import { LoadingSpinner } from './loading-spinner';
2026-01-07 18:39:21 +00:00
// import { ErrorBoundary } from '@/components/ErrorBoundary';
import { logger } from '@/utils/logger';
import { Button } from './button';
import { AlertTriangle, RefreshCw } from 'lucide-react';
// CRITIQUE FIX #16: Composant de fallback amélioré pour les erreurs de chargement lazy
2026-01-07 18:39:21 +00:00
function LazyErrorFallback({
pageName,
error,
onRetry,
2026-01-07 18:39:21 +00:00
}: {
pageName: string;
error?: Error;
onRetry?: () => void;
}) {
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
<div className="flex items-center gap-3 mb-4">
<AlertTriangle className="h-6 w-6 text-kodo-gold" />
<h1 className="text-3xl font-bold">{pageName}</h1>
</div>
<div className="bg-kodo-gold/10 dark:bg-kodo-gold/20 border border-kodo-gold/30 dark:border-kodo-gold/40 text-kodo-gold dark:text-kodo-gold px-4 py-3 rounded-lg mb-4">
<p className="font-medium mb-2">Failed to load {pageName}</p>
{error && (
<p className="text-sm opacity-75">
{error.message || 'An error occurred while loading this page'}
</p>
)}
</div>
<div className="flex gap-3">
{onRetry && (
<Button
onClick={onRetry}
variant="outline"
className="flex items-center gap-2"
>
<RefreshCw className="h-4 w-4" />
Retry
</Button>
)}
2026-01-07 18:39:21 +00:00
<Button
onClick={() => window.location.reload()}
variant="default"
className="flex items-center gap-2"
>
Refresh Page
</Button>
</div>
</div>
</div>
);
}
// CRITIQUE FIX #16: ErrorBoundary spécifique pour les composants lazy
class LazyErrorBoundary extends Component<
2026-01-07 18:39:21 +00:00
{
children: React.ReactNode;
pageName: string;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
},
{ hasError: boolean; error?: Error }
> {
constructor(props: {
children: React.ReactNode;
pageName: string;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// CRITIQUE FIX #16: Logger l'erreur avec le logger centralisé au lieu de console.error
logger.error('[LazyComponent] Failed to load lazy component', {
pageName: this.props.pageName,
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
}
handleRetry = () => {
this.setState({ hasError: false, error: undefined });
};
override render() {
if (this.state.hasError) {
return (
2026-01-07 18:39:21 +00:00
<LazyErrorFallback
pageName={this.props.pageName}
error={this.state.error}
onRetry={this.handleRetry}
/>
);
}
return this.props.children;
}
}
/**
* LazyComponentProps - Propriétés pour les composants lazy créés
*
* @interface LazyComponentProps
*/
interface LazyComponentProps {
/**
* Composant de fallback personnalisé à afficher pendant le chargement
* Si non fourni, utilise LoadingSpinner par défaut
*/
fallback?: React.ReactNode;
}
/**
* createLazyComponent - Factory pour créer des composants lazy avec Suspense
*
* Crée un composant lazy avec gestion automatique du Suspense et du fallback.
* Utile pour le code splitting et le chargement à la demande des composants.
*
* @template T - Type du composant à charger
* @param {() => Promise<{ default: T }>} importFunc - Fonction d'import dynamique
* @param {React.ReactNode} fallback - Composant de fallback (optionnel)
* @returns {ComponentType} Composant wrapper avec Suspense intégré
*
* @example
* ```tsx
* // Créer un composant lazy
* const LazyDashboard = createLazyComponent(
* () => import('@/pages/DashboardPage').then(m => ({ default: m.DashboardPage }))
* );
*
* // Utiliser le composant lazy
* <LazyDashboard fallback={<CustomLoader />} />
* ```
*
* @function
*/
// CRITIQUE FIX #16: Wrapper pour gérer les erreurs de chargement lazy de manière standardisée
function createLazyWithErrorHandling<T extends ComponentType<any>>(
importFunc: () => Promise<{ default: T }>,
pageName: string,
) {
return importFunc().catch((err) => {
// CRITIQUE FIX #16: Logger l'erreur avec le logger centralisé
logger.error('[LazyComponent] Failed to import lazy component', {
pageName,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
2026-01-07 18:39:21 +00:00
// Retourner un composant d'erreur au lieu de laisser l'erreur se propager
2026-01-07 18:39:21 +00:00
return Promise.resolve({
default: () => (
<LazyErrorFallback
pageName={pageName}
error={err instanceof Error ? err : new Error(String(err))}
/>
),
2026-01-07 18:39:21 +00:00
}) as unknown as Promise<{ default: T }>;
});
}
export function createLazyComponent<T extends ComponentType<any>>(
importFunc: () => Promise<{ default: T }>,
2025-12-13 02:34:34 +00:00
fallback?: React.ReactNode,
pageName?: string,
) {
// CRITIQUE FIX #16: Utiliser la fonction avec gestion d'erreur si pageName est fourni
2026-01-07 18:39:21 +00:00
const safeImportFunc = pageName
? () => createLazyWithErrorHandling(importFunc, pageName)
: importFunc;
2026-01-07 18:39:21 +00:00
const LazyComponent = lazy(safeImportFunc);
return function WrappedLazyComponent(
2025-12-13 02:34:34 +00:00
props: React.ComponentProps<T> & LazyComponentProps,
) {
// Extraire fallback des props pour ne pas le passer au composant lazy
const { fallback: _fallback, ...componentProps } = props;
2026-01-07 18:39:21 +00:00
// CRITIQUE FIX #16: Wrapper avec ErrorBoundary pour capturer les erreurs runtime
const component = (
<Suspense fallback={fallback || <LoadingSpinner />}>
{/* @ts-expect-error - LazyComponent props are compatible but TypeScript can't infer it */}
<LazyComponent {...componentProps} />
</Suspense>
);
// Si pageName est fourni, wrapper avec LazyErrorBoundary
if (pageName) {
return (
<LazyErrorBoundary pageName={pageName}>{component}</LazyErrorBoundary>
);
}
return component;
};
}
// Composants lazy communs
// CRITIQUE FIX #16: Ajouter pageName pour tous les composants lazy pour une meilleure gestion d'erreur
export const LazyDashboard = createLazyComponent(
() =>
import('@/pages/DashboardPage').then((m) => ({ default: m.DashboardPage })),
undefined,
'Dashboard',
);
export const LazyChat = createLazyComponent(
() =>
import('@/features/chat/pages/ChatPage').then((m) => ({
default: m.ChatPage,
})),
undefined,
'Chat',
);
// CRITIQUE FIX #16: Tous les composants lazy utilisent maintenant la gestion d'erreur standardisée
export const LazyLibrary = createLazyComponent(
() =>
import('@/features/library/pages/LibraryPage').then((m) => ({
default: m.default,
})),
undefined,
'Library',
);
export const LazyProfile = createLazyComponent(
() => import('@/pages/ProfilePage').then((m) => ({ default: m.ProfilePage })),
undefined,
'Profile',
);
export const LazySettings = createLazyComponent(
() =>
import('@/pages/SettingsPage').then((m) => ({ default: m.SettingsPage })),
undefined,
'Settings',
);
export const LazyLogin = createLazyComponent(
() => import('@/pages/LoginPage').then((m) => ({ default: m.LoginPage })),
undefined,
'Login',
);
export const LazyRegister = createLazyComponent(
() =>
import('@/pages/RegisterPage').then((m) => ({ default: m.RegisterPage })),
undefined,
'Register',
);
export const LazyForgotPassword = createLazyComponent(
2025-12-13 02:34:34 +00:00
() => import('@/features/auth/pages/ForgotPasswordPage'),
undefined,
'Forgot Password',
);
export const LazyVerifyEmail = createLazyComponent(
2025-12-13 02:34:34 +00:00
() => import('@/features/auth/pages/VerifyEmailPage'),
undefined,
'Verify Email',
);
export const LazyResetPassword = createLazyComponent(
2025-12-13 02:34:34 +00:00
() => import('@/features/auth/pages/ResetPasswordPage'),
undefined,
'Reset Password',
);
export const LazySessions = createLazyComponent(
2025-12-13 02:34:34 +00:00
() => import('@/features/auth/pages/SessionsPage'),
undefined,
'Sessions',
);
export const LazyNotFound = createLazyComponent(
2025-12-13 02:34:34 +00:00
() => import('@/features/error/pages/NotFoundPage'),
undefined,
'Not Found',
);
export const LazyServerError = createLazyComponent(
2025-12-13 02:34:34 +00:00
() => import('@/features/error/pages/ServerErrorPage'),
undefined,
'Server Error',
);
export const LazyUserProfile = createLazyComponent(
() =>
import('@/features/profile/pages/UserProfilePage').then((m) => ({
default: m.UserProfilePage,
})),
undefined,
'User Profile',
);
export const LazyRoles = createLazyComponent(
() =>
import('@/features/roles/pages/RolesPage').then((m) => ({
default: m.RolesPage,
})),
undefined,
'Roles',
);
export const LazyTrackDetail = createLazyComponent(
() =>
import('@/features/tracks/pages/TrackDetailPage').then((m) => ({
default: m.TrackDetailPage,
})),
undefined,
'Track Detail',
);
export const LazyPlaylistRoutes = createLazyComponent(
() =>
import('@/features/playlists/routes').then((m) => ({
default: m.PlaylistRoutes,
})),
undefined,
'Playlists',
);
export const LazySearch = createLazyComponent(
() => import('@/pages/SearchPage').then((m) => ({ default: m.SearchPage })),
undefined,
'Search',
);
export const LazyNotifications = createLazyComponent(
() =>
import('@/features/notifications/pages/NotificationsPage').then((m) => ({
default: m.NotificationsPage,
})),
undefined,
'Notifications',
);
export const LazyMarketplace = createLazyComponent(
() =>
import('@/pages/marketplace/MarketplaceHome').then((m) => ({
default: m.MarketplaceHome,
})),
undefined,
'Marketplace',
);
export const LazyAnalytics = createLazyComponent(
() =>
import('@/pages/AnalyticsPage').then((m) => ({ default: m.AnalyticsPage })),
undefined,
'Analytics',
);
export const LazyWebhooks = createLazyComponent(
() =>
import('@/pages/WebhooksPage').then((m) => ({ default: m.WebhooksPage })),
undefined,
'Webhooks',
);
export const LazyAdminDashboard = createLazyComponent(
() =>
import('@/pages/AdminDashboardPage').then((m) => ({
default: m.AdminDashboardPage,
})),
undefined,
'Admin Dashboard',
);
export const LazyDesignSystemDemo = createLazyComponent(
() =>
import('@/pages/DesignSystemDemoPage').then((m) => ({
default: m.default,
})),
undefined,
'Design System Demo',
);
// New pages for navigation fix
export const LazySocial = createLazyComponent(
() => import('@/pages/SocialPage').then((m) => ({ default: m.SocialPage })),
undefined,
'Social Feed',
);
export const LazyGear = createLazyComponent(
() => import('@/pages/GearPage').then((m) => ({ default: m.GearPage })),
undefined,
'Gear Locker',
);
export const LazyLive = createLazyComponent(
() => import('@/pages/LivePage').then((m) => ({ default: m.LivePage })),
undefined,
'Live Sessions',
);
export const LazyEducation = createLazyComponent(
() =>
import('@/pages/EducationPage').then((m) => ({ default: m.EducationPage })),
undefined,
'Education',
);
export const LazyQueue = createLazyComponent(
() => import('@/pages/QueuePage').then((m) => ({ default: m.QueuePage })),
undefined,
'Queue',
);
export const LazyDeveloper = createLazyComponent(
() =>
import('@/pages/DeveloperPage').then((m) => ({ default: m.DeveloperPage })),
undefined,
'Developer API',
);