feat: add batch material uploads and links
This commit is contained in:
@@ -87,6 +87,7 @@ const CourseModal: React.FC<{
|
||||
const [assetConfigured, setAssetConfigured] = useState<boolean | null>(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<Attachment[]>([]);
|
||||
|
||||
// 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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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<{
|
||||
<input
|
||||
ref={attachmentFileRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.csv,.zip,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv,application/zip"
|
||||
className="hidden"
|
||||
onChange={(event) => void handleAttachmentUpload(event)}
|
||||
/>
|
||||
<div className="rounded-xl border border-dashed border-white/15 bg-white/[0.02] p-4">
|
||||
<div
|
||||
onDragEnter={(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]'}`}
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Envie um arquivo de apoio</p>
|
||||
<p className="mt-1 text-xs text-gray-500">PDF, DOCX, XLSX, CSV ou ZIP. O formato e o tamanho são identificados automaticamente.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -700,14 +742,25 @@ const CourseModal: React.FC<{
|
||||
{isAssetUploading ? 'Enviando...' : 'Enviar arquivo'}
|
||||
</button>
|
||||
</div>
|
||||
{assetUploadMessage && <p className={`mt-2 text-xs ${assetUploadMessage.includes('sucesso') ? 'text-emerald-400' : assetUploadMessage.includes('Enviando') ? 'text-gray-400' : 'text-amber-300'}`}>{assetUploadMessage}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-white/5 pt-4 sm:flex-row">
|
||||
<input
|
||||
type="text"
|
||||
value={newAttDesc}
|
||||
onChange={(event) => 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 && <p className={`mt-2 text-xs ${assetUploadMessage.includes('sucesso') ? 'text-emerald-400' : assetUploadMessage.includes('Enviando') ? 'text-gray-400' : 'text-amber-300'}`}>{assetUploadMessage}</p>}
|
||||
<input
|
||||
type="url"
|
||||
value={newLinkUrl}
|
||||
onChange={(event) => 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"
|
||||
/>
|
||||
<button type="button" onClick={handleAddLink} className="rounded-lg border border-white/10 px-4 py-2.5 text-xs font-bold text-white transition hover:bg-white/10">Adicionar link</button>
|
||||
</div>
|
||||
|
||||
{/* List of Attachments */}
|
||||
|
||||
Reference in New Issue
Block a user