feat: add resilient cover previews and zoom
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
MessageSquare,
|
||||
Send,
|
||||
Image as ImageIcon,
|
||||
Minus,
|
||||
PlusCircle,
|
||||
Video,
|
||||
Loader2,
|
||||
FileText,
|
||||
@@ -48,7 +50,11 @@ const CourseModal: React.FC<{
|
||||
const [category, setCategory] = useState('');
|
||||
const [status, setStatus] = useState<Course['status']>('draft');
|
||||
const [thumbnailPreview, setThumbnailPreview] = useState<string>('');
|
||||
const [coverImageUrl, setCoverImageUrl] = useState<string>('');
|
||||
const [coverImageZoom, setCoverImageZoom] = useState(1);
|
||||
const [coverPreviewError, setCoverPreviewError] = useState(false);
|
||||
const coverFileRef = useRef<HTMLInputElement>(null);
|
||||
const localCoverPreviewRef = useRef<string | null>(null);
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [coverConfigured, setCoverConfigured] = useState<boolean | null>(null);
|
||||
const [coverUploadMessage, setCoverUploadMessage] = useState('');
|
||||
@@ -90,6 +96,8 @@ const CourseModal: React.FC<{
|
||||
setCategory(initialData.category);
|
||||
setStatus(initialData.status || 'published');
|
||||
setThumbnailPreview(initialData.thumbnail);
|
||||
setCoverImageUrl(initialData.thumbnail);
|
||||
setCoverImageZoom(initialData.coverImageZoom || 1);
|
||||
setTips(initialData.tips || []);
|
||||
setAttachments(initialData.attachments || []);
|
||||
setLessons(initialData.lessons || []);
|
||||
@@ -99,10 +107,17 @@ const CourseModal: React.FC<{
|
||||
setCategory('');
|
||||
setStatus('draft');
|
||||
setThumbnailPreview('');
|
||||
setCoverImageUrl('');
|
||||
setCoverImageZoom(1);
|
||||
setTips([]);
|
||||
setAttachments([]);
|
||||
setLessons([]);
|
||||
}
|
||||
if (localCoverPreviewRef.current) {
|
||||
URL.revokeObjectURL(localCoverPreviewRef.current);
|
||||
localCoverPreviewRef.current = null;
|
||||
}
|
||||
setCoverPreviewError(false);
|
||||
setNewLessonMedia(null);
|
||||
setBunnyUploadState('idle');
|
||||
setBunnyUploadMessage('');
|
||||
@@ -122,6 +137,10 @@ const CourseModal: React.FC<{
|
||||
}
|
||||
}, [isOpen, initialData]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (localCoverPreviewRef.current) URL.revokeObjectURL(localCoverPreviewRef.current);
|
||||
}, []);
|
||||
|
||||
const toBunnyMedia = (video: BunnyVideo): LessonMedia => ({
|
||||
provider: 'bunny',
|
||||
externalId: video.id,
|
||||
@@ -175,7 +194,12 @@ const CourseModal: React.FC<{
|
||||
setCoverUploadMessage(`Enviando ${file.name}...`);
|
||||
try {
|
||||
const response = await instructorApi.uploadCover(file);
|
||||
setThumbnailPreview(response.data.coverImageUrl);
|
||||
if (localCoverPreviewRef.current) URL.revokeObjectURL(localCoverPreviewRef.current);
|
||||
const localPreviewUrl = URL.createObjectURL(file);
|
||||
localCoverPreviewRef.current = localPreviewUrl;
|
||||
setThumbnailPreview(localPreviewUrl);
|
||||
setCoverImageUrl(response.data.coverImageUrl);
|
||||
setCoverPreviewError(false);
|
||||
setCoverUploadMessage('Imagem de capa enviada com sucesso.');
|
||||
} catch (error) {
|
||||
setCoverUploadMessage(error instanceof Error ? error.message : 'Não foi possível enviar a imagem de capa.');
|
||||
@@ -351,7 +375,8 @@ const CourseModal: React.FC<{
|
||||
description,
|
||||
category: category || categories[0],
|
||||
status,
|
||||
thumbnail: thumbnailPreview || '/course-placeholder.svg',
|
||||
thumbnail: coverImageUrl || '/course-placeholder.svg',
|
||||
coverImageZoom,
|
||||
duration: durationStr,
|
||||
instructor: initialData?.instructor || '',
|
||||
views: initialData?.views || 0,
|
||||
@@ -411,9 +436,40 @@ const CourseModal: React.FC<{
|
||||
>
|
||||
{thumbnailPreview ? (
|
||||
<>
|
||||
<img src={thumbnailPreview} alt="Preview" className="w-full h-full object-cover opacity-70 group-hover:opacity-40 transition-opacity" />
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="bg-orange-500 px-4 py-2 rounded-xl text-xs font-bold shadow-lg">Alterar Capa</span>
|
||||
{coverPreviewError ? (
|
||||
<div className="p-6 text-center">
|
||||
<ImageIcon className="mx-auto mb-2 h-9 w-9 text-amber-400" />
|
||||
<p className="text-sm font-semibold text-white">Não foi possível carregar a imagem</p>
|
||||
<p className="mt-1 text-xs text-gray-400">Tente enviá-la novamente. Se o problema continuar, verifique a configuração de entrega de imagens.</p>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={thumbnailPreview}
|
||||
alt="Prévia da imagem de capa"
|
||||
onError={() => setCoverPreviewError(true)}
|
||||
className="h-full w-full object-cover opacity-80 transition-transform duration-200"
|
||||
style={{ transform: `scale(${coverImageZoom})` }}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-x-3 bottom-3 flex items-center justify-between gap-3">
|
||||
<span className="rounded-lg bg-black/70 px-3 py-2 text-xs font-semibold text-white backdrop-blur-sm">Clique na imagem para alterar</span>
|
||||
<div className="flex items-center rounded-lg border border-white/10 bg-black/70 p-1 backdrop-blur-sm">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Reduzir zoom da imagem"
|
||||
disabled={coverImageZoom <= 1}
|
||||
onClick={(event) => { event.stopPropagation(); setCoverImageZoom((current) => Math.max(1, Number((current - 0.1).toFixed(1)))); }}
|
||||
className="rounded-md p-1.5 text-white transition-colors hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
><Minus className="h-4 w-4" /></button>
|
||||
<span className="min-w-12 text-center text-[11px] font-bold text-white">{Math.round(coverImageZoom * 100)}%</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Aumentar zoom da imagem"
|
||||
disabled={coverImageZoom >= 2.5}
|
||||
onClick={(event) => { event.stopPropagation(); setCoverImageZoom((current) => Math.min(2.5, Number((current + 0.1).toFixed(1)))); }}
|
||||
className="rounded-md p-1.5 text-white transition-colors hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
><PlusCircle className="h-4 w-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -929,6 +985,7 @@ export const ManageCourses: React.FC = () => {
|
||||
src={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
style={{ transform: `scale(${course.coverImageZoom || 1})` }}
|
||||
/>
|
||||
<div className="absolute top-3 left-3 bg-black/70 backdrop-blur-md px-2.5 py-1 rounded-[980px] text-caption font-semibold uppercase tracking-caption text-white border border-white/10">
|
||||
{course.category}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const MyLearning: React.FC<{ onPlay: (course: Course) => void }> = ({ onP
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
{courses.map((course) => (
|
||||
<button key={course.id} onClick={() => onPlay(course)} className="group overflow-hidden rounded-2xl border border-white/10 bg-zinc-950/80 text-left hover:border-orange-500/50 transition-colors">
|
||||
<div className="aspect-video relative bg-zinc-900"><img src={course.thumbnail} alt="" className="h-full w-full object-cover opacity-75 group-hover:opacity-100 transition-opacity" /><div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent" /><span className="absolute left-4 bottom-4 inline-flex items-center gap-2 rounded-full bg-orange-500 px-3 py-2 text-xs font-bold text-white"><PlayCircle className="w-4 h-4" /> Continuar</span></div>
|
||||
<div className="aspect-video relative overflow-hidden bg-zinc-900"><img src={course.thumbnail} alt="" className="h-full w-full object-cover opacity-75 transition-opacity group-hover:opacity-100" style={{ transform: `scale(${course.coverImageZoom || 1})` }} /><div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent" /><span className="absolute left-4 bottom-4 inline-flex items-center gap-2 rounded-full bg-orange-500 px-3 py-2 text-xs font-bold text-white"><PlayCircle className="w-4 h-4" /> Continuar</span></div>
|
||||
<div className="p-5"><p className="text-xs text-orange-400 font-semibold uppercase tracking-wider">{course.category}</p><h2 className="mt-2 font-bold text-white">{course.title}</h2><div className="mt-4 h-1.5 overflow-hidden rounded-full bg-white/10"><div className="h-full rounded-full bg-orange-500" style={{ width: `${course.progress || 0}%` }} /></div><p className="mt-2 text-xs text-gray-400">{course.progress || 0}% concluído</p></div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user