| | import React, { useState, useEffect } from "react"; |
| | import { motion, AnimatePresence } from "framer-motion"; |
| | import { Upload, FileText, Image, FileSpreadsheet, X, Sparkles, AlertCircle } from "lucide-react"; |
| | import { cn } from "@/lib/utils"; |
| | import { Input } from "@/components/ui/input"; |
| |
|
| | |
| | const ALLOWED_TYPES = [ |
| | "application/pdf", |
| | "image/png", |
| | "image/jpeg", |
| | "image/jpg", |
| | "image/tiff", |
| | "image/tif" |
| | ]; |
| |
|
| | |
| | const ALLOWED_EXTENSIONS = [".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"]; |
| |
|
| | |
| | const MAX_FILE_SIZE = 4 * 1024 * 1024; |
| |
|
| | export default function UploadZone({ onFileSelect, selectedFile, onClear, keyFields = "", onKeyFieldsChange = () => {} }) { |
| | const [isDragging, setIsDragging] = useState(false); |
| | const [error, setError] = useState(null); |
| |
|
| | const validateFile = (file) => { |
| | |
| | setError(null); |
| |
|
| | |
| | const fileExtension = "." + file.name.split(".").pop().toLowerCase(); |
| | const isValidType = ALLOWED_TYPES.includes(file.type) || ALLOWED_EXTENSIONS.includes(fileExtension); |
| | |
| | if (!isValidType) { |
| | setError("Only PDF, PNG, JPG, and TIFF files are allowed."); |
| | return false; |
| | } |
| |
|
| | |
| | if (file.size > MAX_FILE_SIZE) { |
| | const fileSizeMB = (file.size / 1024 / 1024).toFixed(2); |
| | setError(`File size exceeds 4 MB limit. Your file is ${fileSizeMB} MB.`); |
| | return false; |
| | } |
| |
|
| | return true; |
| | }; |
| |
|
| | const handleFileSelect = (file) => { |
| | if (validateFile(file)) { |
| | setError(null); |
| | onFileSelect(file); |
| | } |
| | }; |
| |
|
| | const handleDragOver = (e) => { |
| | e.preventDefault(); |
| | setIsDragging(true); |
| | }; |
| |
|
| | const handleDragLeave = () => { |
| | setIsDragging(false); |
| | }; |
| |
|
| | const handleDrop = (e) => { |
| | e.preventDefault(); |
| | setIsDragging(false); |
| | const file = e.dataTransfer.files[0]; |
| | if (file) { |
| | handleFileSelect(file); |
| | } |
| | }; |
| |
|
| | const getFileIcon = (type) => { |
| | if (type?.includes("image")) return Image; |
| | if (type?.includes("spreadsheet") || type?.includes("excel")) return FileSpreadsheet; |
| | return FileText; |
| | }; |
| |
|
| | const FileIcon = selectedFile ? getFileIcon(selectedFile.type) : FileText; |
| |
|
| | |
| | useEffect(() => { |
| | if (!selectedFile) { |
| | setError(null); |
| | } |
| | }, [selectedFile]); |
| |
|
| | return ( |
| | <div className="w-full"> |
| | <AnimatePresence mode="wait"> |
| | {!selectedFile ? ( |
| | <motion.div |
| | key="upload" |
| | initial={{ opacity: 0, y: 10 }} |
| | animate={{ opacity: 1, y: 0 }} |
| | exit={{ opacity: 0, y: -10 }} |
| | transition={{ duration: 0.2 }} |
| | onDragOver={handleDragOver} |
| | onDragLeave={handleDragLeave} |
| | onDrop={handleDrop} |
| | className={cn( |
| | "relative group cursor-pointer", |
| | "border-2 border-dashed rounded-2xl", |
| | "transition-all duration-300 ease-out", |
| | isDragging |
| | ? "border-indigo-400 bg-indigo-50/50" |
| | : "border-slate-200 hover:border-indigo-300 hover:bg-slate-50/50" |
| | )} |
| | > |
| | <label className="flex flex-col items-center justify-center py-16 px-8 cursor-pointer"> |
| | <motion.div |
| | animate={isDragging ? { scale: 1.1, y: -5 } : { scale: 1, y: 0 }} |
| | className={cn( |
| | "h-16 w-16 rounded-2xl flex items-center justify-center mb-6 transition-colors duration-300", |
| | isDragging |
| | ? "bg-indigo-100" |
| | : "bg-gradient-to-br from-slate-100 to-slate-50 group-hover:from-indigo-100 group-hover:to-violet-50" |
| | )} |
| | > |
| | <Upload |
| | className={cn( |
| | "h-7 w-7 transition-colors duration-300", |
| | isDragging ? "text-indigo-600" : "text-slate-400 group-hover:text-indigo-500" |
| | )} |
| | /> |
| | </motion.div> |
| | |
| | <div className="text-center"> |
| | <p className="text-lg font-semibold text-slate-700 mb-1"> |
| | {isDragging ? "Drop your file here" : "Drop your file here, or browse"} |
| | </p> |
| | <p className="text-sm text-slate-400"> |
| | Supports PDF, PNG, JPG, TIFF up to 4MB |
| | </p> |
| | </div> |
| | |
| | <div className="flex items-center gap-2 mt-6"> |
| | <div className="flex -space-x-1"> |
| | {[ |
| | "bg-red-100 text-red-600", |
| | "bg-blue-100 text-blue-600", |
| | "bg-green-100 text-green-600", |
| | "bg-amber-100 text-amber-600", |
| | ].map((color, i) => ( |
| | <div |
| | key={i} |
| | className={`h-8 w-8 rounded-lg ${color.split(" ")[0]} flex items-center justify-center border-2 border-white`} |
| | > |
| | <FileText className={`h-4 w-4 ${color.split(" ")[1]}`} /> |
| | </div> |
| | ))} |
| | </div> |
| | <span className="text-xs text-slate-400 ml-2">Multiple formats supported</span> |
| | </div> |
| | |
| | <input |
| | type="file" |
| | className="hidden" |
| | accept=".pdf,.png,.jpg,.jpeg,.tiff,.tif" |
| | onChange={(e) => { |
| | const file = e.target.files[0]; |
| | if (file) { |
| | handleFileSelect(file); |
| | } |
| | // Reset input so same file can be selected again after error |
| | e.target.value = ""; |
| | }} |
| | /> |
| | </label> |
| | |
| | {/* Decorative gradient border on hover */} |
| | <div className="absolute inset-0 -z-10 rounded-2xl bg-gradient-to-r from-indigo-500 via-violet-500 to-purple-500 opacity-0 group-hover:opacity-10 blur-xl transition-opacity duration-500" /> |
| | </motion.div> |
| | ) : ( |
| | <motion.div |
| | key="selected" |
| | initial={{ opacity: 0, scale: 0.95 }} |
| | animate={{ opacity: 1, scale: 1 }} |
| | exit={{ opacity: 0, scale: 0.95 }} |
| | className="grid grid-cols-1 lg:grid-cols-2 gap-3" |
| | > |
| | {/* File Info Box */} |
| | <div className="relative bg-gradient-to-br from-indigo-50 to-violet-50 rounded-xl p-3 border border-indigo-100"> |
| | <div className="flex items-center gap-3"> |
| | <div className="h-10 w-10 rounded-lg bg-white shadow-sm flex items-center justify-center flex-shrink-0"> |
| | <FileIcon className="h-5 w-5 text-indigo-600" /> |
| | </div> |
| | <div className="flex-1 min-w-0"> |
| | <p className="font-medium text-slate-800 truncate text-sm">{selectedFile.name}</p> |
| | <div className="flex items-center gap-2 text-xs text-slate-500"> |
| | <span>{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</span> |
| | <span className="text-indigo-500">•</span> |
| | <span className="text-indigo-600 flex items-center gap-1"> |
| | <Sparkles className="h-3 w-3" /> |
| | Ready for extraction |
| | </span> |
| | </div> |
| | </div> |
| | <button |
| | onClick={onClear} |
| | className="h-8 w-8 rounded-lg bg-white hover:bg-red-50 border border-slate-200 hover:border-red-200 flex items-center justify-center text-slate-400 hover:text-red-500 transition-colors" |
| | > |
| | <X className="h-4 w-4" /> |
| | </button> |
| | </div> |
| | </div> |
| | |
| | {/* Key Fields Box */} |
| | <div className="relative bg-white rounded-xl p-3 border border-slate-200"> |
| | <label className="block text-xs font-medium text-slate-600 mb-1.5"> |
| | <span className="font-bold">Key Fields</span> <span className="font-normal">(if required)</span> |
| | </label> |
| | <Input |
| | type="text" |
| | value={keyFields || ""} |
| | onChange={(e) => { |
| | if (onKeyFieldsChange) { |
| | onKeyFieldsChange(e.target.value); |
| | } |
| | }} |
| | placeholder="Invoice Number, Invoice Date, PO Number, Supplier Name, Total Amount, Payment terms, Additional Notes" |
| | className="h-8 text-xs border-slate-200 focus:border-indigo-300 focus:ring-indigo-200" |
| | /> |
| | </div> |
| | </motion.div> |
| | )} |
| | </AnimatePresence> |
| | |
| | {/* Error Message */} |
| | {error && ( |
| | <motion.div |
| | initial={{ opacity: 0, y: -10 }} |
| | animate={{ opacity: 1, y: 0 }} |
| | exit={{ opacity: 0, y: -10 }} |
| | className="mt-3 p-3 bg-red-50 border border-red-200 rounded-xl flex items-start gap-2" |
| | > |
| | <AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0 mt-0.5" /> |
| | <p className="text-sm text-red-700 flex-1">{error}</p> |
| | <button |
| | onClick={() => setError(null)} |
| | className="text-red-600 hover:text-red-800 transition-colors" |
| | > |
| | <X className="h-4 w-4" /> |
| | </button> |
| | </motion.div> |
| | )} |
| | </div> |
| | ); |
| | } |
| |
|