import axios from "axios"; import { FileUp, XCircle } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { toast } from "sonner"; import { useRequestPresignedImport } from "@/api/generated/content/content.ts"; import { Button } from "@/components/ui/button.tsx"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog.tsx"; import { Input } from "@/components/ui/input.tsx"; import { Progress } from "@/components/ui/progress.tsx"; interface MangaManualImportDialogProps { fileImportDialogOpen: boolean; onFileImportDialogOpenChange: (open: boolean) => void; } export const MangaManualImportDialog = ({ fileImportDialogOpen, onFileImportDialogOpenChange, }: MangaManualImportDialogProps) => { const [malId, setMalId] = useState(""); const [aniListId, setAniListId] = useState(""); const [dragActive, setDragActive] = useState(false); const [files, setFiles] = useState(null); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState>( {}, ); const [uploadErrors, setUploadErrors] = useState>({}); const { mutateAsync: requestPresignedUrl } = useRequestPresignedImport(); const handleFileImport = async () => { if (!files || files.length === 0) { toast.error("Please select one or more files to upload"); return; } if (!malId.trim() && !aniListId.trim()) { toast.error("Please enter either an AniList or a MyAnimeList ID"); return; } setIsUploading(true); setUploadProgress({}); setUploadErrors({}); let hasError = false; for (const file of files) { try { const response = await requestPresignedUrl({ data: { malId: malId ? Number(malId) : undefined, aniListId: aniListId ? Number(aniListId) : undefined, originalFilename: file.name, }, }); const presignedUrl = response?.data?.presignedUrl; if (!presignedUrl) { throw new Error("Failed to get presigned URL"); } await axios.put(presignedUrl, file, { headers: { "Content-Type": "", }, onUploadProgress: (progressEvent) => { if (progressEvent.total) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total, ); setUploadProgress((prev) => ({ ...prev, [file.name]: percentCompleted, })); } }, }); } catch (error) { console.error(`Error uploading ${file.name}:`, error); hasError = true; setUploadErrors((prev) => ({ ...prev, [file.name]: true })); toast.error(`Failed to import ${file.name}`); } } setIsUploading(false); if (!hasError) { setFiles(null); setMalId(""); setAniListId(""); setUploadProgress({}); setUploadErrors({}); onFileImportDialogOpenChange(false); toast.success("Manga imported successfully! Backend will process it."); } }; const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") { setDragActive(true); } else if (e.type === "dragleave") { setDragActive(false); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); if (e.dataTransfer.files) { setFiles(Array.from(e.dataTransfer.files)); } }; const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { setFiles(Array.from(e.target.files)); } }; return ( Import from File Upload one or more files and provide the MyAnimeList manga URL (or ID) to import manga data.
setAniListId(e.target.value)} className="mt-2" disabled={isUploading} />
setMalId(e.target.value)} className="mt-2" disabled={isUploading} />
{!isUploading && (
)} {isUploading && files && files.length > 0 && (
Overall Progress {Math.round( files.reduce( (acc, file) => acc + (uploadProgress[file.name] || 0), 0, ) / files.length, )} %
acc + (uploadProgress[file.name] || 0), 0, ) / files.length } indicatorClassName={ Math.round( files.reduce( (acc, file) => acc + (uploadProgress[file.name] || 0), 0, ) / files.length, ) === 100 ? "bg-green-500" : "" } />
{files.map((file) => { const isError = uploadErrors[file.name]; const progress = uploadProgress[file.name] || 0; return (
{file.name} {isError ? ( Failed ) : ( {progress}% )}
); })}
)}
); };