diff --git a/pages/ManageCourses.tsx b/pages/ManageCourses.tsx index 18e1f53..4b97af9 100644 --- a/pages/ManageCourses.tsx +++ b/pages/ManageCourses.tsx @@ -87,6 +87,7 @@ const CourseModal: React.FC<{ const [assetConfigured, setAssetConfigured] = useState(null); const [isAssetUploading, setIsAssetUploading] = useState(false); const [assetUploadMessage, setAssetUploadMessage] = useState(''); + const [isDraggingAsset, setIsDraggingAsset] = useState(false); const [editorStep, setEditorStep] = useState<1 | 2 | 3>(1); // Lesson Management @@ -106,8 +107,8 @@ const CourseModal: React.FC<{ const [newTip, setNewTip] = useState(''); const [attachments, setAttachments] = useState([]); - // Optional context saved alongside each uploaded material. - const [newAttDesc, setNewAttDesc] = useState(''); + const [newLinkName, setNewLinkName] = useState(''); + const [newLinkUrl, setNewLinkUrl] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); @@ -152,7 +153,8 @@ const CourseModal: React.FC<{ setBunnyUploadMessage(''); setCoverUploadMessage(''); setAssetUploadMessage(''); - setNewAttDesc(''); + setNewLinkName(''); + setNewLinkUrl(''); instructorApi.bunnyConfiguration() .then((response) => setBunnyConfigured(response.data.configured)) .catch(() => setBunnyConfigured(false)); @@ -396,40 +398,74 @@ const CourseModal: React.FC<{ setTips(tips.filter((_, i) => i !== idx)); }; - const handleAttachmentUpload = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - event.target.value = ''; - if (!file) return; + const handleAttachmentUploads = async (files: FileList | File[]) => { + const selectedFiles = Array.from(files); + if (!selectedFiles.length) return; if (!assetConfigured) { setAssetUploadMessage('O armazenamento de materiais ainda não foi configurado pelo administrador.'); return; } setIsAssetUploading(true); - setAssetUploadMessage(`Enviando ${file.name}...`); + setAssetUploadMessage(selectedFiles.length === 1 ? `Enviando ${selectedFiles[0].name}...` : `Enviando ${selectedFiles.length} arquivos...`); + const uploadedAttachments: Attachment[] = []; + const failedFiles: string[] = []; + for (const file of selectedFiles) { + try { + const response = await instructorApi.uploadAsset(file); + const type: AttachmentType = response.data.kind === 'spreadsheet' + ? 'spreadsheet' + : response.data.kind === 'archive' + ? 'zip' + : file.name.toLowerCase().endsWith('.pdf') + ? 'pdf' + : 'doc'; + uploadedAttachments.push({ + id: `asset-${response.data.key}`, + name: file.name, + type, + url: response.data.assetUrl, + sizeBytes: response.data.sizeBytes, + description: '', + }); + } catch { + failedFiles.push(file.name); + } + } + if (uploadedAttachments.length) setAttachments((current) => [...current, ...uploadedAttachments]); + setAssetUploadMessage(failedFiles.length + ? `${uploadedAttachments.length} enviado(s). Não foi possível enviar: ${failedFiles.join(', ')}.` + : `${uploadedAttachments.length} material(is) enviado(s) com sucesso.`); + setIsAssetUploading(false); + }; + + const handleAttachmentUpload = async (event: React.ChangeEvent) => { + const files = event.target.files; + event.target.value = ''; + if (!files) return; + await handleAttachmentUploads(files); + }; + + const handleAddLink = () => { + const url = newLinkUrl.trim(); + if (!url) { + alert('Informe o link do material.'); + return; + } try { - const response = await instructorApi.uploadAsset(file); - const type: AttachmentType = response.data.kind === 'spreadsheet' - ? 'spreadsheet' - : response.data.kind === 'archive' - ? 'zip' - : file.name.toLowerCase().endsWith('.pdf') - ? 'pdf' - : 'doc'; + const parsedUrl = new URL(url); + if (!['https:', 'http:'].includes(parsedUrl.protocol)) throw new Error(); setAttachments((current) => [...current, { - id: `asset-${response.data.key}`, - name: file.name, - type, - url: response.data.assetUrl, - sizeBytes: response.data.sizeBytes, - description: newAttDesc.trim(), + id: `link-${Date.now()}`, + name: newLinkName.trim() || parsedUrl.hostname, + type: 'link', + url, + description: '', }]); - setNewAttDesc(''); - setAssetUploadMessage('Material enviado com sucesso.'); - } catch (error) { - setAssetUploadMessage(error instanceof Error ? error.message : 'Não foi possível enviar o material.'); - } finally { - setIsAssetUploading(false); + setNewLinkName(''); + setNewLinkUrl(''); + } catch { + alert('Informe um link HTTP ou HTTPS válido.'); } }; @@ -680,15 +716,21 @@ const CourseModal: React.FC<{ void handleAttachmentUpload(event)} /> -
+
{ event.preventDefault(); setIsDraggingAsset(true); }} + onDragOver={(event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; setIsDraggingAsset(true); }} + onDragLeave={(event) => { event.preventDefault(); setIsDraggingAsset(false); }} + onDrop={(event) => { event.preventDefault(); setIsDraggingAsset(false); void handleAttachmentUploads(event.dataTransfer.files); }} + className={`rounded-xl border border-dashed p-5 transition ${isDraggingAsset ? 'border-orange-400 bg-orange-500/10' : 'border-white/15 bg-white/[0.02]'}`} + >

Envie um arquivo de apoio

-

PDF, DOCX, XLSX, CSV ou ZIP. O formato e o tamanho são identificados automaticamente.

+ {assetUploadMessage &&

{assetUploadMessage}

} +
+ +
setNewAttDesc(event.target.value)} - placeholder="Descrição opcional: como este material apoia o aprendizado?" - className="mt-3 w-full rounded-lg border border-white/10 bg-black/20 px-3 py-2.5 text-xs text-white outline-none focus:border-orange-500/50" + value={newLinkName} + onChange={(event) => setNewLinkName(event.target.value)} + placeholder="Título do link (opcional)" + className="min-w-0 rounded-lg border border-white/10 bg-white/5 px-3 py-2.5 text-xs text-white outline-none focus:border-orange-500/50 sm:w-52" /> - {assetUploadMessage &&

{assetUploadMessage}

} + setNewLinkUrl(event.target.value)} + placeholder="https://..." + className="min-w-0 flex-1 rounded-lg border border-white/10 bg-white/5 px-3 py-2.5 text-xs text-white outline-none focus:border-orange-500/50" + /> +
{/* List of Attachments */}