2025-12-03 21:56:50 +00:00
|
|
|
/**
|
|
|
|
|
* 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'
|
|
|
|
|
) {
|
2025-12-03 21:56:50 +00:00
|
|
|
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'
|
|
|
|
|
) {
|
2025-12-03 21:56:50 +00:00
|
|
|
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 {
|
2025-12-03 21:56:50 +00:00
|
|
|
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,
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
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',
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
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 && (
|
2025-12-03 21:56:50 +00:00
|
|
|
<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;
|