feat: simplify instructor video uploads
This commit is contained in:
@@ -23,8 +23,12 @@ import {
|
|||||||
Lock,
|
Lock,
|
||||||
Unlock,
|
Unlock,
|
||||||
Globe,
|
Globe,
|
||||||
Download
|
Download,
|
||||||
,Copy, ChevronUp, ChevronDown, ChevronLeft, ChevronRight
|
Copy,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Course, Comment, Attachment, Lesson, AttachmentType, LessonMedia } from '../types';
|
import { Course, Comment, Attachment, Lesson, AttachmentType, LessonMedia } from '../types';
|
||||||
import { useToast } from '../context/ToastContext';
|
import { useToast } from '../context/ToastContext';
|
||||||
@@ -57,8 +61,6 @@ const CourseModal: React.FC<{
|
|||||||
// New Lesson State
|
// New Lesson State
|
||||||
const [newLessonTitle, setNewLessonTitle] = useState('');
|
const [newLessonTitle, setNewLessonTitle] = useState('');
|
||||||
const [newLessonDuration, setNewLessonDuration] = useState('');
|
const [newLessonDuration, setNewLessonDuration] = useState('');
|
||||||
const [newLessonVideoUrl, setNewLessonVideoUrl] = useState('');
|
|
||||||
const [newLessonProvider, setNewLessonProvider] = useState('external');
|
|
||||||
const [newLessonMedia, setNewLessonMedia] = useState<LessonMedia | null>(null);
|
const [newLessonMedia, setNewLessonMedia] = useState<LessonMedia | null>(null);
|
||||||
const [newLessonIsFree, setNewLessonIsFree] = useState(false);
|
const [newLessonIsFree, setNewLessonIsFree] = useState(false);
|
||||||
const [bunnyConfigured, setBunnyConfigured] = useState<boolean | null>(null);
|
const [bunnyConfigured, setBunnyConfigured] = useState<boolean | null>(null);
|
||||||
@@ -136,12 +138,12 @@ const CourseModal: React.FC<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!bunnyConfigured) {
|
if (!bunnyConfigured) {
|
||||||
alert('A integração com Bunny Stream ainda não foi configurada pelo administrador.');
|
alert('O envio de vídeos ainda não está disponível. Tente novamente mais tarde ou entre em contato com o administrador.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBunnyUploadState('uploading');
|
setBunnyUploadState('uploading');
|
||||||
setBunnyUploadMessage(`Enviando ${file.name} para Bunny Stream...`);
|
setBunnyUploadMessage(`Enviando ${file.name}...`);
|
||||||
try {
|
try {
|
||||||
const created = await instructorApi.createBunnyVideo(newLessonTitle.trim());
|
const created = await instructorApi.createBunnyVideo(newLessonTitle.trim());
|
||||||
const uploaded = await instructorApi.uploadBunnyVideo(created.data.id, file);
|
const uploaded = await instructorApi.uploadBunnyVideo(created.data.id, file);
|
||||||
@@ -154,10 +156,10 @@ const CourseModal: React.FC<{
|
|||||||
setBunnyUploadState('uploaded');
|
setBunnyUploadState('uploaded');
|
||||||
setBunnyUploadMessage(uploaded.data.status === 'ready'
|
setBunnyUploadMessage(uploaded.data.status === 'ready'
|
||||||
? 'Vídeo pronto para ser adicionado à aula.'
|
? 'Vídeo pronto para ser adicionado à aula.'
|
||||||
: 'Upload concluído. Bunny Stream está processando o vídeo; você já pode salvar a aula.');
|
: 'Upload concluído. O vídeo está sendo processado; você já pode salvar a aula.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setBunnyUploadState('error');
|
setBunnyUploadState('error');
|
||||||
setBunnyUploadMessage(error instanceof Error ? error.message : 'Não foi possível enviar o vídeo para Bunny Stream.');
|
setBunnyUploadMessage('Não foi possível enviar o vídeo. Tente novamente.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,8 +240,8 @@ const CourseModal: React.FC<{
|
|||||||
alert("Digite um título para a aula.");
|
alert("Digite um título para a aula.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!newLessonMedia && !newLessonVideoUrl.trim()) {
|
if (!newLessonMedia) {
|
||||||
alert('Envie um vídeo para Bunny Stream ou informe uma URL de vídeo hospedado.');
|
alert('Envie o vídeo da aula antes de adicioná-la.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,9 +249,9 @@ const CourseModal: React.FC<{
|
|||||||
id: `lesson-${Date.now()}`,
|
id: `lesson-${Date.now()}`,
|
||||||
title: newLessonTitle,
|
title: newLessonTitle,
|
||||||
duration: newLessonDuration || '15m',
|
duration: newLessonDuration || '15m',
|
||||||
videoUrl: newLessonMedia?.playbackUrl || newLessonVideoUrl,
|
videoUrl: newLessonMedia.playbackUrl || '',
|
||||||
mediaProvider: newLessonMedia?.provider || newLessonProvider || 'external',
|
mediaProvider: newLessonMedia.provider,
|
||||||
media: newLessonMedia ? [newLessonMedia] : undefined,
|
media: [newLessonMedia],
|
||||||
isFree: newLessonIsFree,
|
isFree: newLessonIsFree,
|
||||||
description: ''
|
description: ''
|
||||||
};
|
};
|
||||||
@@ -258,8 +260,6 @@ const CourseModal: React.FC<{
|
|||||||
|
|
||||||
setNewLessonTitle('');
|
setNewLessonTitle('');
|
||||||
setNewLessonDuration('');
|
setNewLessonDuration('');
|
||||||
setNewLessonVideoUrl('');
|
|
||||||
setNewLessonProvider('external');
|
|
||||||
setNewLessonMedia(null);
|
setNewLessonMedia(null);
|
||||||
setNewLessonIsFree(false);
|
setNewLessonIsFree(false);
|
||||||
setBunnyUploadState('idle');
|
setBunnyUploadState('idle');
|
||||||
@@ -431,7 +431,7 @@ const CourseModal: React.FC<{
|
|||||||
disabled={!coverConfigured || isCoverUploading}
|
disabled={!coverConfigured || isCoverUploading}
|
||||||
onChange={handleCoverUpload}
|
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>
|
<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 envio de imagens estará disponível assim que o armazenamento de mídia for configurado.')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -441,7 +441,7 @@ const CourseModal: React.FC<{
|
|||||||
type="text"
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Ex: Masterclass: Escala de Tráfego Pago"
|
placeholder="Ex.: Estratégias de tráfego pago para e-commerce"
|
||||||
className="w-full bg-black/50 text-white text-base font-bold border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50"
|
className="w-full bg-black/50 text-white text-base font-bold border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -477,7 +477,7 @@ const CourseModal: React.FC<{
|
|||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="O que os alunos vão aprender e dominar neste curso?"
|
placeholder="Descreva os tópicos, as competências e os resultados esperados para os alunos."
|
||||||
className="w-full bg-black/50 text-white border border-white/10 rounded-xl px-4 py-2.5 text-xs focus:outline-none focus:border-orange-500/50 resize-none leading-relaxed"
|
className="w-full bg-black/50 text-white border border-white/10 rounded-xl px-4 py-2.5 text-xs focus:outline-none focus:border-orange-500/50 resize-none leading-relaxed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,7 +504,7 @@ const CourseModal: React.FC<{
|
|||||||
type="text"
|
type="text"
|
||||||
value={newAttName}
|
value={newAttName}
|
||||||
onChange={(e) => setNewAttName(e.target.value)}
|
onChange={(e) => setNewAttName(e.target.value)}
|
||||||
placeholder="Nome do arquivo (ex: Planilha_ROAS.xlsx)"
|
placeholder="Ex.: Planilha de acompanhamento de campanhas"
|
||||||
className="col-span-2 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none focus:border-orange-500/50"
|
className="col-span-2 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none focus:border-orange-500/50"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
@@ -525,14 +525,14 @@ const CourseModal: React.FC<{
|
|||||||
type="text"
|
type="text"
|
||||||
value={newAttSize}
|
value={newAttSize}
|
||||||
onChange={(e) => setNewAttSize(e.target.value)}
|
onChange={(e) => setNewAttSize(e.target.value)}
|
||||||
placeholder="Tamanho (ex: 2.4 MB)"
|
placeholder="Ex.: 2,4 MB"
|
||||||
className="w-28 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none"
|
className="w-28 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newAttDesc}
|
value={newAttDesc}
|
||||||
onChange={(e) => setNewAttDesc(e.target.value)}
|
onChange={(e) => setNewAttDesc(e.target.value)}
|
||||||
placeholder="Breve descrição do material..."
|
placeholder="Explique como este material apoia o aprendizado."
|
||||||
className="flex-1 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none"
|
className="flex-1 bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -605,7 +605,7 @@ const CourseModal: React.FC<{
|
|||||||
<span className="flex items-center gap-1"><Video className="w-3 h-3 text-orange-400/70"/> {lesson.duration}</span>
|
<span className="flex items-center gap-1"><Video className="w-3 h-3 text-orange-400/70"/> {lesson.duration}</span>
|
||||||
{lesson.media?.[0]?.provider === 'bunny' && (
|
{lesson.media?.[0]?.provider === 'bunny' && (
|
||||||
<span className={`font-bold px-1.5 py-0.2 rounded text-[9px] uppercase ${lesson.media[0].status === 'ready' ? 'text-emerald-400 bg-emerald-500/10' : lesson.media[0].status === 'failed' ? 'text-red-400 bg-red-500/10' : 'text-amber-400 bg-amber-500/10'}`}>
|
<span className={`font-bold px-1.5 py-0.2 rounded text-[9px] uppercase ${lesson.media[0].status === 'ready' ? 'text-emerald-400 bg-emerald-500/10' : lesson.media[0].status === 'failed' ? 'text-red-400 bg-red-500/10' : 'text-amber-400 bg-amber-500/10'}`}>
|
||||||
Bunny: {lesson.media[0].status === 'ready' ? 'pronto' : lesson.media[0].status === 'failed' ? 'falhou' : 'processando'}
|
Vídeo {lesson.media[0].status === 'ready' ? 'pronto' : lesson.media[0].status === 'failed' ? 'com erro' : 'processando'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{lesson.isFree ? (
|
{lesson.isFree ? (
|
||||||
@@ -653,7 +653,7 @@ const CourseModal: React.FC<{
|
|||||||
<div className="p-4 bg-zinc-950 border-t border-white/5 space-y-3">
|
<div className="p-4 bg-zinc-950 border-t border-white/5 space-y-3">
|
||||||
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider flex items-center gap-1.5">
|
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
<Plus className="w-3.5 h-3.5 text-orange-400" />
|
<Plus className="w-3.5 h-3.5 text-orange-400" />
|
||||||
Cadastrar Nova Aula
|
Adicionar aula
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -661,14 +661,14 @@ const CourseModal: React.FC<{
|
|||||||
type="text"
|
type="text"
|
||||||
value={newLessonTitle}
|
value={newLessonTitle}
|
||||||
onChange={(e) => setNewLessonTitle(e.target.value)}
|
onChange={(e) => setNewLessonTitle(e.target.value)}
|
||||||
placeholder="Título da Aula (ex: 01. Configuração do Pixel)"
|
placeholder="Ex.: Aula 01 — Configuração inicial do Pixel"
|
||||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-orange-500/50"
|
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-orange-500/50"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newLessonDuration}
|
value={newLessonDuration}
|
||||||
onChange={(e) => setNewLessonDuration(e.target.value)}
|
onChange={(e) => setNewLessonDuration(e.target.value)}
|
||||||
placeholder="Duração (ex: 25m)"
|
placeholder="Ex.: 25 min"
|
||||||
className="w-28 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-orange-500/50"
|
className="w-28 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-orange-500/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -676,11 +676,11 @@ const CourseModal: React.FC<{
|
|||||||
<div className="rounded-xl border border-orange-500/20 bg-orange-500/[0.04] p-3 space-y-2">
|
<div className="rounded-xl border border-orange-500/20 bg-orange-500/[0.04] p-3 space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold text-orange-200">Vídeo Bunny Stream</p>
|
<p className="text-xs font-bold text-orange-200">Vídeo da aula</p>
|
||||||
<p className="text-[11px] text-gray-500">O vídeo será protegido e reproduzido no player seguro da Bunny.</p>
|
<p className="text-[11px] text-gray-500">Envie o arquivo para adicioná-lo à aula. O processamento ocorre automaticamente.</p>
|
||||||
</div>
|
</div>
|
||||||
<label className={`cursor-pointer rounded-lg px-3 py-2 text-xs font-bold transition-colors ${bunnyConfigured && bunnyUploadState !== 'uploading' ? 'bg-orange-500 hover:bg-orange-600 text-white' : 'bg-white/10 text-gray-500 cursor-not-allowed'}`}>
|
<label className={`cursor-pointer rounded-lg px-3 py-2 text-xs font-bold transition-colors ${bunnyConfigured && bunnyUploadState !== 'uploading' ? 'bg-orange-500 hover:bg-orange-600 text-white' : 'bg-white/10 text-gray-500 cursor-not-allowed'}`}>
|
||||||
{bunnyUploadState === 'uploading' ? 'Enviando...' : 'Selecionar vídeo'}
|
{bunnyUploadState === 'uploading' ? 'Enviando...' : 'Enviar vídeo'}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="video/*"
|
accept="video/*"
|
||||||
@@ -690,32 +690,17 @@ const CourseModal: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{bunnyConfigured === false && <p className="text-[11px] text-amber-300">Bunny Stream não está configurado no servidor. Peça ao administrador para adicionar as variáveis no Portainer.</p>}
|
{bunnyConfigured === false && <p className="text-[11px] text-amber-300">O envio de vídeos está indisponível no momento. Entre em contato com o administrador.</p>}
|
||||||
{bunnyUploadMessage && <p className={`text-[11px] ${bunnyUploadState === 'error' ? 'text-red-300' : bunnyUploadState === 'uploaded' ? 'text-emerald-300' : 'text-gray-400'}`}>{bunnyUploadMessage}</p>}
|
{bunnyUploadMessage && <p className={`text-[11px] ${bunnyUploadState === 'error' ? 'text-red-300' : bunnyUploadState === 'uploaded' ? 'text-emerald-300' : 'text-gray-400'}`}>{bunnyUploadMessage}</p>}
|
||||||
{newLessonMedia && (
|
{newLessonMedia && (
|
||||||
<div className="flex items-center justify-between rounded-lg bg-black/30 px-3 py-2 text-[11px] text-gray-300">
|
<div className="flex items-center justify-between rounded-lg bg-black/30 px-3 py-2 text-[11px] text-gray-300">
|
||||||
<span>Vídeo Bunny vinculado ({newLessonMedia.status === 'ready' ? 'pronto' : 'processando'})</span>
|
<span>Vídeo enviado ({newLessonMedia.status === 'ready' ? 'pronto' : 'processando'})</span>
|
||||||
<button type="button" onClick={() => setNewLessonMedia(null)} className="text-gray-400 hover:text-red-300">Remover</button>
|
<button type="button" onClick={() => setNewLessonMedia(null)} className="text-gray-400 hover:text-red-300">Remover</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 items-center pt-1">
|
<div className="flex justify-end pt-1">
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newLessonProvider}
|
|
||||||
onChange={(event) => setNewLessonProvider(event.target.value)}
|
|
||||||
placeholder="Provider externo"
|
|
||||||
className="w-40 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
value={newLessonVideoUrl}
|
|
||||||
onChange={(event) => setNewLessonVideoUrl(event.target.value)}
|
|
||||||
placeholder="Ou cole uma URL externa de vídeo"
|
|
||||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddLesson}
|
onClick={handleAddLesson}
|
||||||
@@ -1035,7 +1020,7 @@ export const ManageCourses: React.FC = () => {
|
|||||||
<textarea
|
<textarea
|
||||||
value={replyText}
|
value={replyText}
|
||||||
onChange={(e) => setReplyText(e.target.value)}
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
placeholder="Escreva a resposta para o aluno..."
|
placeholder="Escreva uma resposta clara para o aluno..."
|
||||||
className="w-full bg-black/60 border border-white/10 rounded-[8px] p-3 text-body-sm text-white focus:outline-none focus:border-orange-500/50 resize-none"
|
className="w-full bg-black/60 border border-white/10 rounded-[8px] p-3 text-body-sm text-white focus:outline-none focus:border-orange-500/50 resize-none"
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user