40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
|
|
type CourseCoverImageProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'onError'> & {
|
|
source: string;
|
|
onFinalError?: () => void;
|
|
};
|
|
|
|
const fallbackCoverUrl = (source: string) => {
|
|
try {
|
|
const parsed = new URL(source, window.location.origin);
|
|
const filename = parsed.pathname.split('/').pop() || '';
|
|
if (!/^[0-9a-f]{8}-[0-9a-f-]{27}\.(?:jpg|png|webp)$/i.test(filename)) return null;
|
|
const apiBase = import.meta.env.VITE_API_URL || '/api/v1';
|
|
return `${apiBase}/courses/covers/${filename}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const CourseCoverImage: React.FC<CourseCoverImageProps> = ({ source, onFinalError, ...props }) => {
|
|
const fallbackUrl = useMemo(() => fallbackCoverUrl(source), [source]);
|
|
const [usingFallback, setUsingFallback] = useState(false);
|
|
|
|
useEffect(() => setUsingFallback(false), [source]);
|
|
|
|
return (
|
|
<img
|
|
{...props}
|
|
src={usingFallback && fallbackUrl ? fallbackUrl : source}
|
|
onError={() => {
|
|
if (!usingFallback && fallbackUrl) {
|
|
setUsingFallback(true);
|
|
return;
|
|
}
|
|
onFinalError?.();
|
|
}}
|
|
/>
|
|
);
|
|
};
|