veza/apps/web/src/features/player/components/PlayerError.tsx

141 lines
3.7 KiB
TypeScript
Raw Normal View History

/**
* Composant PlayerError
* Affiche les erreurs du player avec messages utilisateur et bouton retry
*/
import React from 'react';
import { AlertCircle, RefreshCw } from 'lucide-react';
import { cn } from '@/lib/utils';
export type PlayerErrorType =
| 'network'
| 'decode'
| 'source'
| 'abort'
| 'unknown';
export interface PlayerErrorProps {
error: Error | null;
errorType?: PlayerErrorType;
onRetry?: () => void;
className?: string;
showRetry?: boolean;
retryLabel?: string;
}
const ERROR_MESSAGES: Record<PlayerErrorType, string> = {
network: 'Erreur de connexion. Vérifiez votre connexion internet.',
decode: 'Erreur de décodage audio. Le fichier est peut-être corrompu.',
source: 'Erreur de source audio. Le fichier est introuvable ou inaccessible.',
abort: 'Chargement annulé.',
unknown: 'Une erreur est survenue lors de la lecture.',
};
function getErrorType(error: Error | null): PlayerErrorType {
if (!error) return 'unknown';
const message = error.message.toLowerCase();
const name = error.name.toLowerCase();
2025-12-13 02:34:34 +00:00
if (
message.includes('network') ||
message.includes('fetch') ||
name === 'networkerror'
) {
return 'network';
}
if (message.includes('decode') || name === 'decodeerror') {
return 'decode';
}
2025-12-13 02:34:34 +00:00
if (
message.includes('source') ||
message.includes('not found') ||
name === 'notfounderror'
) {
return 'source';
}
if (message.includes('abort') || name === 'aborterror') {
return 'abort';
}
return 'unknown';
}
2025-12-13 02:34:34 +00:00
function getErrorMessage(
error: Error | null,
errorType?: PlayerErrorType,
): string {
const type = errorType || getErrorType(error);
return ERROR_MESSAGES[type] || ERROR_MESSAGES.unknown;
}
export function PlayerError({
error,
errorType,
onRetry,
className,
showRetry = true,
retryLabel = 'Réessayer',
}: PlayerErrorProps) {
if (!error) {
return null;
}
const message = getErrorMessage(error, errorType);
const type = errorType || getErrorType(error);
return (
<div
className={cn(
'flex flex-col items-center justify-center gap-4 p-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg',
2025-12-13 02:34:34 +00:00
className,
)}
role="alert"
aria-live="assertive"
>
<div className="flex items-center gap-3">
<AlertCircle
className="h-6 w-6 text-red-600 dark:text-red-400"
aria-hidden="true"
/>
<div className="flex flex-col gap-1">
<h3 className="text-sm font-semibold text-red-900 dark:text-red-200">
Erreur de lecture
</h3>
<p className="text-sm text-red-700 dark:text-red-300">{message}</p>
</div>
</div>
{showRetry && onRetry && (
<button
type="button"
onClick={onRetry}
className={cn(
'flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg',
'hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2',
'dark:bg-red-500 dark:hover:bg-red-600',
2025-12-13 02:34:34 +00:00
'transition-colors',
)}
aria-label={`${retryLabel}: ${message}`}
>
<RefreshCw className="h-4 w-4" aria-hidden="true" />
<span>{retryLabel}</span>
</button>
)}
2025-12-17 13:07:35 +00:00
{import.meta.env.DEV && (
<details className="w-full mt-2">
<summary className="text-xs text-red-600 dark:text-red-400 cursor-pointer">
Détails techniques
</summary>
<pre className="mt-2 p-2 text-xs bg-red-100 dark:bg-red-900/40 rounded overflow-auto">
{error.stack || error.message}
</pre>
</details>
)}
</div>
);
}
export default PlayerError;