fix: fall back to Bunny Storage for cover images
All checks were successful
CI / Validate frontend and API (push) Successful in 51s
CI / Build and publish Docker images (push) Successful in 21s

This commit is contained in:
Cauê Faleiros
2026-09-04 15:26:06 -03:00
parent 66671f65c3
commit ed00f60281
8 changed files with 90 additions and 12 deletions

View File

@@ -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}

View 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?.();
}}
/>
);
};

View File

@@ -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={{

View File

@@ -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={{