feat: add Bunny Storage course cover uploads
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user