fix: fall back to Bunny Storage for cover images
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { PlayCircle, Download, FileText, CheckCircle2 } from 'lucide-react';
|
||||
import { Course, SectionVariant } from '../types';
|
||||
import { CourseCoverImage } from './CourseCoverImage';
|
||||
|
||||
interface CourseCardProps {
|
||||
course: Course;
|
||||
@@ -37,8 +38,8 @@ export const CourseCard: React.FC<CourseCardProps> = ({
|
||||
onClick={() => onClick?.(course)}
|
||||
className={`relative ${aspectRatioClass} w-full overflow-hidden rounded-[8px] bg-zinc-900 shadow-xl transition-all duration-300 ease-out group-hover:scale-[1.02] group-hover:shadow-2xl group-hover:shadow-orange-500/10 group-hover:z-10 ring-0 ring-white/0 group-hover:ring-2 group-hover:ring-orange-500/40`}
|
||||
>
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
<CourseCoverImage
|
||||
source={course.thumbnail}
|
||||
alt={course.title}
|
||||
loading="lazy"
|
||||
draggable={false}
|
||||
|
||||
39
components/CourseCoverImage.tsx
Normal file
39
components/CourseCoverImage.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
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?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Play, Plus, Sparkles } from 'lucide-react';
|
||||
import { Course } from '../types';
|
||||
import { getCourses } from '../services/db';
|
||||
import { CourseCoverImage } from './CourseCoverImage';
|
||||
|
||||
interface HeroProps {
|
||||
onPlay?: (course: Course) => void;
|
||||
@@ -20,8 +21,8 @@ export const Hero: React.FC<HeroProps> = ({ onPlay }) => {
|
||||
<div className="relative w-full min-h-[75vh] lg:min-h-[82vh] max-h-[800px] h-auto overflow-hidden group flex flex-col justify-end pt-28 pb-12 sm:pb-16 px-6 md:px-12">
|
||||
{/* Background Image with Cinematic Gradient Overlay */}
|
||||
<div className="absolute inset-0 transition-transform duration-[20s] ease-in-out group-hover:scale-105">
|
||||
<img
|
||||
src={featuredCourse.thumbnail}
|
||||
<CourseCoverImage
|
||||
source={featuredCourse.thumbnail}
|
||||
alt={featuredCourse.title}
|
||||
className="w-full h-full object-cover object-center"
|
||||
style={{
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Search, X, Loader2, PlayCircle, Download } from 'lucide-react';
|
||||
import { Course } from '../types';
|
||||
import { getCourses } from '../services/db';
|
||||
import { CourseCoverImage } from './CourseCoverImage';
|
||||
|
||||
interface SearchModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -132,8 +133,8 @@ export const SearchModal: React.FC<SearchModalProps> = ({ isOpen, onClose, onPla
|
||||
className="cursor-pointer group bg-zinc-900/60 rounded-[8px] p-3 border border-white/5 hover:border-orange-500/40 transition-all duration-300 shadow-xl"
|
||||
>
|
||||
<div className="aspect-video bg-gray-900 rounded-[8px] overflow-hidden mb-3 relative">
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
<CourseCoverImage
|
||||
source={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="w-full h-full object-cover opacity-80 group-hover:opacity-100 group-hover:scale-105 transition-all duration-300 rounded-[8px]"
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user