|
|
| "use client"; |
|
|
| import { useState, useRef } from "react"; |
| import axios from "axios"; |
| import { useSession } from "next-auth/react"; |
|
|
| interface FileConverterProps { |
| accessToken: string; |
| } |
|
|
| export default function FileConverter({ accessToken }: FileConverterProps) { |
| const [files, setFiles] = useState<File[]>([]); |
| const [isUploading, setIsUploading] = useState(false); |
| const [progress, setProgress] = useState(0); |
| const [outputName, setOutputName] = useState("converted_document"); |
| const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
| const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| if (e.target.files) { |
| setFiles(prev => [...prev, ...Array.from(e.target.files!)]); |
| } |
| }; |
|
|
| const handleDrop = (e: React.DragEvent) => { |
| e.preventDefault(); |
| if (e.dataTransfer.files) { |
| setFiles(prev => [...prev, ...Array.from(e.dataTransfer.files)]); |
| } |
| }; |
|
|
| const removeFile = (index: number) => { |
| setFiles(prev => prev.filter((_, i) => i !== index)); |
| }; |
|
|
| const handleUpload = async () => { |
| if (files.length === 0) return; |
|
|
| setIsUploading(true); |
| setProgress(10); |
|
|
| const formData = new FormData(); |
| formData.append("token", accessToken); |
| formData.append("filename", outputName); |
| files.forEach((file) => { |
| formData.append("files", file); |
| }); |
|
|
| try { |
| setProgress(30); |
| const response = await axios.post("/py-api/convert-local-files", formData, { |
| responseType: "blob", |
| params: { combine: false }, |
| onUploadProgress: (progressEvent) => { |
| const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1)); |
| setProgress(30 + (percentCompleted * 0.6)); |
| } |
| }); |
|
|
| setProgress(100); |
| |
| const url = window.URL.createObjectURL(new Blob([response.data])); |
| const link = document.createElement("a"); |
| link.href = url; |
| link.setAttribute("download", `${outputName}.zip`); |
| document.body.appendChild(link); |
| link.click(); |
| link.remove(); |
| |
| setTimeout(() => { |
| setIsUploading(false); |
| setProgress(0); |
| }, 2000); |
|
|
| } catch (error) { |
| console.error("Conversion failed:", error); |
| alert("Conversion failed. Please try again."); |
| setIsUploading(false); |
| setProgress(0); |
| } |
| }; |
|
|
| return ( |
| <div className="w-full max-w-3xl bg-white/80 backdrop-blur-md rounded-2xl shadow-xl border border-white/20 p-8 transition-all duration-300 hover:shadow-2xl"> |
| <h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2"> |
| <span className="bg-blue-500 text-white p-2 rounded-lg"> |
| <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /> |
| </svg> |
| </span> |
| Quick Converter |
| </h2> |
| |
| <div |
| className="border-2 border-dashed border-blue-200 rounded-xl p-10 flex flex-col items-center justify-center bg-blue-50/30 hover:bg-blue-50/50 transition-colors cursor-pointer" |
| onClick={() => fileInputRef.current?.click()} |
| onDragOver={(e) => e.preventDefault()} |
| onDrop={handleDrop} |
| > |
| <input |
| type="file" |
| multiple |
| hidden |
| ref={fileInputRef} |
| onChange={handleFileChange} |
| accept=".docx,.pptx,.ipynb" |
| /> |
| <div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center mb-4"> |
| <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> |
| </svg> |
| </div> |
| <p className="text-gray-600 font-medium">Click to upload or drag & drop</p> |
| <p className="text-gray-400 text-sm mt-1">Supports DOCX, PPTX, IPYNB</p> |
| </div> |
| |
| {files.length > 0 && ( |
| <div className="mt-8 space-y-4"> |
| <div className="flex justify-between items-center"> |
| <h3 className="font-semibold text-gray-700">Selected Files ({files.length})</h3> |
| <button |
| onClick={() => setFiles([])} |
| className="text-sm text-red-500 hover:underline" |
| > |
| Clear All |
| </button> |
| </div> |
| |
| <div className="max-h-60 overflow-y-auto space-y-2 pr-2"> |
| {files.map((file, index) => ( |
| <div key={index} className="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-100 group animate-in fade-in slide-in-from-left-2 duration-300"> |
| <div className="flex items-center gap-3"> |
| <div className="w-8 h-8 flex items-center justify-center bg-white rounded shadow-sm"> |
| {file.name.endsWith('.docx') && <span className="text-blue-500 text-xs font-bold">DOC</span>} |
| {file.name.endsWith('.pptx') && <span className="text-orange-500 text-xs font-bold">PPT</span>} |
| {file.name.endsWith('.ipynb') && <span className="text-yellow-600 text-xs font-bold">NB</span>} |
| </div> |
| <span className="text-sm text-gray-700 truncate max-w-[200px] md:max-w-md">{file.name}</span> |
| </div> |
| <button |
| onClick={(e) => { e.stopPropagation(); removeFile(index); }} |
| className="text-gray-400 hover:text-red-500 p-1 rounded-full hover:bg-red-50 transition-colors" |
| > |
| <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> |
| <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /> |
| </svg> |
| </button> |
| </div> |
| ))} |
| </div> |
| |
| <div className="pt-4 border-t border-gray-100 space-y-4"> |
| <div> |
| <label className="block text-sm font-medium text-gray-700 mb-1">Output PDF Name</label> |
| <input |
| type="text" |
| value={outputName} |
| onChange={(e) => setOutputName(e.target.value)} |
| className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all" |
| placeholder="Name your file..." |
| /> |
| </div> |
| |
| <button |
| onClick={handleUpload} |
| disabled={isUploading || files.length === 0} |
| className={`w-full py-3 rounded-xl font-bold text-white transition-all transform active:scale-95 flex items-center justify-center gap-2 ${ |
| isUploading ? 'bg-blue-400 cursor-not-allowed' : 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg hover:shadow-blue-200' |
| }`} |
| > |
| {isUploading ? ( |
| <> |
| <svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
| </svg> |
| Processing... {Math.round(progress)}% |
| </> |
| ) : ( |
| <> |
| Convert to Separate PDFs (ZIP) |
| </> |
| )} |
| </button> |
| </div> |
| </div> |
| )} |
|
|
| {isUploading && ( |
| <div className="mt-4 w-full bg-gray-100 h-2 rounded-full overflow-hidden"> |
| <div |
| className="bg-blue-500 h-full transition-all duration-300" |
| style={{ width: `${progress}%` }} |
| /> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|