SPARKNET / frontend /components /PatentUpload.tsx
MHamdan's picture
Initial commit: SPARKNET framework
a9dc537
'use client';
import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { motion, AnimatePresence } from 'framer-motion';
import { Upload, FileText, X, Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
import { formatFileSize } from '@/lib/api';
interface PatentUploadProps {
onUpload: (file: File) => Promise<void>;
uploading?: boolean;
error?: string | null;
}
export function PatentUpload({ onUpload, uploading = false, error = null }: PatentUploadProps) {
const [file, setFile] = useState<File | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
setFile(acceptedFiles[0]);
}
}, []);
const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
onDrop,
accept: {
'application/pdf': ['.pdf'],
},
maxSize: 50 * 1024 * 1024, // 50MB
multiple: false,
});
const handleUpload = async () => {
console.log('🚀 handleUpload called!');
console.log('File:', file);
if (!file) {
console.error('❌ No file selected!');
return;
}
try {
console.log('📤 Starting upload for:', file.name);
// Simulate progress for UX (actual upload is handled by parent)
setUploadProgress(0);
const interval = setInterval(() => {
setUploadProgress((prev) => {
if (prev >= 90) {
clearInterval(interval);
return 90;
}
return prev + 10;
});
}, 200);
console.log('📡 Calling onUpload callback...');
await onUpload(file);
clearInterval(interval);
setUploadProgress(100);
console.log('✅ Upload completed!');
} catch (err) {
console.error('❌ Upload failed:', err);
}
};
const handleRemoveFile = () => {
setFile(null);
setUploadProgress(0);
};
return (
<div className="w-full max-w-2xl mx-auto space-y-4">
{/* Dropzone */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card
{...getRootProps()}
className={cn(
'border-2 border-dashed p-12 text-center cursor-pointer transition-all',
isDragActive && 'border-blue-500 bg-blue-50 scale-105',
isDragReject && 'border-red-500 bg-red-50',
!isDragActive && !isDragReject && 'border-gray-300 hover:border-blue-400 hover:bg-gray-50',
uploading && 'pointer-events-none opacity-50'
)}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center space-y-4">
<motion.div
animate={{
scale: isDragActive ? 1.1 : 1,
rotate: isDragActive ? 5 : 0,
}}
transition={{ duration: 0.2 }}
>
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-blue-100 to-purple-100">
<Upload className="h-10 w-10 text-blue-600" />
</div>
</motion.div>
{isDragReject ? (
<div className="text-red-600">
<p className="font-medium">Invalid file type</p>
<p className="text-sm">Only PDF files up to 50MB are accepted</p>
</div>
) : isDragActive ? (
<div className="text-blue-600">
<p className="text-lg font-medium">Drop your patent here</p>
</div>
) : (
<div className="space-y-2">
<p className="text-lg font-medium text-gray-900">
Drag & drop your patent PDF here
</p>
<p className="text-sm text-gray-500">
or click to browse files (Max 50MB)
</p>
</div>
)}
<div className="flex items-center space-x-4 text-xs text-gray-400">
<div className="flex items-center space-x-1">
<FileText className="h-4 w-4" />
<span>PDF only</span>
</div>
<div className="h-4 w-px bg-gray-300" />
<span>Max 50MB</span>
</div>
</div>
</Card>
</motion.div>
{/* Selected File Display */}
<AnimatePresence>
{file && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
>
<Card className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3 flex-1 min-w-0">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-blue-50">
<FileText className="h-6 w-6 text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 truncate">{file.name}</p>
<p className="text-sm text-gray-500">{formatFileSize(file.size)}</p>
</div>
</div>
{!uploading && uploadProgress === 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleRemoveFile}
className="shrink-0"
>
<X className="h-4 w-4" />
</Button>
)}
{uploading && (
<Loader2 className="h-5 w-5 animate-spin text-blue-600 shrink-0" />
)}
{uploadProgress === 100 && (
<CheckCircle2 className="h-5 w-5 text-green-600 shrink-0" />
)}
</div>
{/* Upload Progress */}
{uploading && uploadProgress > 0 && uploadProgress < 100 && (
<div className="mt-3 space-y-1">
<Progress value={uploadProgress} className="h-2" />
<p className="text-xs text-gray-500 text-right">{uploadProgress}%</p>
</div>
)}
</Card>
</motion.div>
)}
</AnimatePresence>
{/* Error Display */}
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="border-red-200 bg-red-50 p-4">
<div className="flex items-start space-x-3">
<AlertCircle className="h-5 w-5 text-red-600 shrink-0 mt-0.5" />
<div>
<p className="font-medium text-red-900">Upload Failed</p>
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</Card>
</motion.div>
)}
{/* Upload Button */}
{file && !uploading && uploadProgress === 0 && (
<div>
<Button
onClick={() => {
console.log('🔴 BUTTON CLICKED!');
alert('Button clicked! Check console.');
handleUpload();
}}
disabled={uploading}
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 h-12 text-base font-medium"
>
{uploading ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="mr-2 h-5 w-5" />
Upload & Analyze Patent
</>
)}
</Button>
</div>
)}
</div>
);
}