feat: add Bunny Storage course cover uploads
All checks were successful
CI / Validate frontend and API (push) Successful in 44s
CI / Build and publish Docker images (push) Successful in 22s

This commit is contained in:
Cauê Faleiros
2026-09-04 13:39:30 -03:00
parent 3498d47182
commit e15a44e733
11 changed files with 195 additions and 14 deletions

View File

@@ -44,8 +44,11 @@ const CourseModal: React.FC<{
const [category, setCategory] = useState('');
const [status, setStatus] = useState<Course['status']>('draft');
const [thumbnailPreview, setThumbnailPreview] = useState<string>('');
const coverUrlRef = useRef<HTMLInputElement>(null);
const coverFileRef = useRef<HTMLInputElement>(null);
const [categories, setCategories] = useState<string[]>([]);
const [coverConfigured, setCoverConfigured] = useState<boolean | null>(null);
const [coverUploadMessage, setCoverUploadMessage] = useState('');
const [isCoverUploading, setIsCoverUploading] = useState(false);
// Lesson Management
const [lessons, setLessons] = useState<Lesson[]>([]);
@@ -99,9 +102,13 @@ const CourseModal: React.FC<{
setNewLessonMedia(null);
setBunnyUploadState('idle');
setBunnyUploadMessage('');
setCoverUploadMessage('');
instructorApi.bunnyConfiguration()
.then((response) => setBunnyConfigured(response.data.configured))
.catch(() => setBunnyConfigured(false));
instructorApi.coverConfiguration()
.then((response) => setCoverConfigured(response.data.configured))
.catch(() => setCoverConfigured(false));
courseApi.categories()
.then((response) => {
setCategories(response.data);
@@ -152,6 +159,27 @@ const CourseModal: React.FC<{
}
};
const handleCoverUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = '';
if (!file) return;
if (!coverConfigured) {
setCoverUploadMessage('O armazenamento de imagens ainda não foi configurado pelo administrador.');
return;
}
setIsCoverUploading(true);
setCoverUploadMessage(`Enviando ${file.name}...`);
try {
const response = await instructorApi.uploadCover(file);
setThumbnailPreview(response.data.coverImageUrl);
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.');
} finally {
setIsCoverUploading(false);
}
};
const refreshLessonMedia = async (lessonId: string) => {
const lesson = lessons.find((item) => item.id === lessonId);
const media = lesson?.media?.[0];
@@ -345,12 +373,12 @@ const CourseModal: React.FC<{
<div>
<label className="text-xs text-gray-400 uppercase font-bold tracking-wider mb-2 block">Imagem de Capa (16:9)</label>
<div
onClick={() => coverUrlRef.current?.focus()}
onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') coverUrlRef.current?.focus(); }}
onClick={() => { if (!isCoverUploading && coverConfigured) coverFileRef.current?.click(); }}
onKeyDown={(event) => { if ((event.key === 'Enter' || event.key === ' ') && !isCoverUploading && coverConfigured) coverFileRef.current?.click(); }}
role="button"
tabIndex={0}
aria-label="Informar URL da imagem de capa"
className="w-full aspect-video rounded-2xl border-2 border-dashed border-white/10 bg-black/40 flex flex-col items-center justify-center cursor-pointer hover:border-orange-500/50 transition-colors relative overflow-hidden group"
aria-label="Enviar imagem de capa"
className={`w-full aspect-video rounded-2xl border-2 border-dashed border-white/10 bg-black/40 flex flex-col items-center justify-center transition-colors relative overflow-hidden group ${coverConfigured && !isCoverUploading ? 'cursor-pointer hover:border-orange-500/50' : 'cursor-not-allowed opacity-70'}`}
>
{thumbnailPreview ? (
<>
@@ -362,18 +390,19 @@ const CourseModal: React.FC<{
) : (
<div className="text-center p-6">
<ImageIcon className="w-10 h-10 text-orange-400/60 mx-auto mb-2" />
<p className="text-gray-300 text-xs font-medium">Clique para informar a URL da imagem de capa</p>
<p className="text-gray-300 text-xs font-medium">{isCoverUploading ? 'Enviando imagem...' : 'Clique para enviar uma imagem de capa'}</p>
</div>
)}
</div>
<input
ref={coverUrlRef}
type="url"
value={thumbnailPreview}
onChange={(event) => setThumbnailPreview(event.target.value)}
placeholder="Cole a URL pública da imagem de capa"
className="w-full mt-2 bg-black/50 text-white text-xs border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50"
ref={coverFileRef}
type="file"
accept="image/jpeg,image/png,image/webp"
className="hidden"
disabled={!coverConfigured || isCoverUploading}
onChange={handleCoverUpload}
/>
<p className={`mt-2 text-[11px] ${coverUploadMessage.includes('sucesso') ? 'text-emerald-300' : coverUploadMessage ? 'text-amber-300' : 'text-gray-500'}`}>{coverUploadMessage || (coverConfigured ? 'JPG, PNG ou WebP. Tamanho máximo: 10 MB.' : 'O upload de capas será habilitado após configurar Bunny Storage no servidor.')}</p>
</div>
<div className="space-y-4">