api / src /components /FileUpload.tsx
OhMyDitzzy
test
c0ee8b0
import { useState, useRef, useCallback } from "react";
import { Upload, X, File, AlertCircle, Link as LinkIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { FileConstraints } from "@/server/types/plugin";
interface FileUploadProps {
name: string;
description: string;
fileConstraints?: FileConstraints;
acceptUrl?: boolean;
onFileChange: (file: File | null) => void;
onUrlChange?: (url: string) => void;
disabled?: boolean;
}
export function FileUpload({
name,
description,
fileConstraints,
acceptUrl = false,
onFileChange,
onUrlChange,
disabled = false,
}: FileUploadProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [fileUrl, setFileUrl] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [dragActive, setDragActive] = useState(false);
const [mode, setMode] = useState<"upload" | "url">("upload");
const fileInputRef = useRef<HTMLInputElement>(null);
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
};
const validateFile = (file: File): string | null => {
if (!fileConstraints) return null;
if (fileConstraints.maxSize && file.size > fileConstraints.maxSize) {
return `File size (${formatFileSize(file.size)}) exceeds maximum allowed size (${formatFileSize(fileConstraints.maxSize)})`;
}
if (fileConstraints.acceptedTypes && fileConstraints.acceptedTypes.length > 0) {
if (!fileConstraints.acceptedTypes.includes(file.type)) {
return `File type ${file.type} is not accepted. Allowed types: ${fileConstraints.acceptedTypes.join(", ")}`;
}
}
if (fileConstraints.acceptedExtensions && fileConstraints.acceptedExtensions.length > 0) {
const extension = "." + file.name.split(".").pop()?.toLowerCase();
if (!fileConstraints.acceptedExtensions.includes(extension)) {
return `File extension ${extension} is not accepted. Allowed extensions: ${fileConstraints.acceptedExtensions.join(", ")}`;
}
}
return null;
};
const handleFileSelect = useCallback((file: File) => {
const validationError = validateFile(file);
if (validationError) {
setError(validationError);
setSelectedFile(null);
onFileChange(null);
return;
}
setError(null);
setSelectedFile(file);
onFileChange(file);
}, [fileConstraints, onFileChange]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFileSelect(file);
}
};
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 && e.dataTransfer.files[0]) {
handleFileSelect(e.dataTransfer.files[0]);
}
};
const handleRemoveFile = () => {
setSelectedFile(null);
setError(null);
onFileChange(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleUrlChange = (url: string) => {
setFileUrl(url);
if (onUrlChange) {
onUrlChange(url);
}
};
const getAcceptAttribute = (): string | undefined => {
if (!fileConstraints) return undefined;
const types = [];
if (fileConstraints.acceptedTypes) {
types.push(...fileConstraints.acceptedTypes);
}
if (fileConstraints.acceptedExtensions) {
types.push(...fileConstraints.acceptedExtensions);
}
return types.length > 0 ? types.join(",") : undefined;
};
const renderContent = () => {
if (!acceptUrl) {
return (
<div className="space-y-3">
{renderUploadArea()}
{renderConstraints()}
</div>
);
}
return (
<Tabs value={mode} onValueChange={(v) => setMode(v as "upload" | "url")} className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-black/30">
<TabsTrigger value="upload" className="data-[state=active]:bg-purple-500/20">
<Upload className="w-4 h-4 mr-2" />
Upload File
</TabsTrigger>
<TabsTrigger value="url" className="data-[state=active]:bg-purple-500/20">
<LinkIcon className="w-4 h-4 mr-2" />
Use URL
</TabsTrigger>
</TabsList>
<TabsContent value="upload" className="mt-3 space-y-3">
{renderUploadArea()}
{renderConstraints()}
</TabsContent>
<TabsContent value="url" className="mt-3 space-y-3">
<Input
type="url"
placeholder="https://example.com/file.jpg"
value={fileUrl}
onChange={(e) => handleUrlChange(e.target.value)}
disabled={disabled}
className="bg-black/50 border-white/10 text-white focus:border-purple-500"
/>
<p className="text-xs text-gray-500">
Enter the URL of the file you want to process
</p>
{renderConstraints()}
</TabsContent>
</Tabs>
);
};
const renderUploadArea = () => (
<>
<div
className={`relative border-2 border-dashed rounded-lg p-8 transition-all ${
dragActive
? "border-purple-500 bg-purple-500/10"
: "border-white/20 bg-black/30"
} ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:border-purple-500/50"}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
onClick={() => !disabled && fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
onChange={handleFileChange}
accept={getAcceptAttribute()}
disabled={disabled}
className="hidden"
/>
{selectedFile ? (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-500/20 rounded">
<File className="w-6 h-6 text-purple-400" />
</div>
<div>
<p className="text-sm font-medium text-white">{selectedFile.name}</p>
<p className="text-xs text-gray-400">{formatFileSize(selectedFile.size)}</p>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleRemoveFile();
}}
disabled={disabled}
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
<X className="w-4 h-4" />
</Button>
</div>
) : (
<div className="text-center">
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<p className="text-sm text-gray-300 mb-1">
{dragActive ? "Drop file here" : "Click to upload or drag and drop"}
</p>
<p className="text-xs text-gray-500">{description}</p>
</div>
)}
</div>
{error && (
<div className="flex items-start gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-300">{error}</p>
</div>
)}
</>
);
const renderConstraints = () => {
if (!fileConstraints) return null;
return (
<div className="space-y-2">
{fileConstraints.maxSize && (
<p className="text-xs text-gray-500">
• Max file size: {formatFileSize(fileConstraints.maxSize)}
</p>
)}
{fileConstraints.acceptedTypes && fileConstraints.acceptedTypes.length > 0 && (
<p className="text-xs text-gray-500">
• Accepted types: {fileConstraints.acceptedTypes.join(", ")}
</p>
)}
{fileConstraints.acceptedExtensions && fileConstraints.acceptedExtensions.length > 0 && (
<p className="text-xs text-gray-500">
• Accepted extensions: {fileConstraints.acceptedExtensions.join(", ")}
</p>
)}
</div>
);
};
return (
<div className="space-y-2">
<label className="block text-sm text-gray-300">
{name}
<span className="text-xs text-gray-500 ml-2">(file)</span>
</label>
{renderContent()}
</div>
);
}