OmniHelper / client /src /components /FileUploadZone.tsx
hotboxxgenn's picture
Upload 9 files (#12)
c2d42ec verified
import { useCallback, useState } from "react";
import { Upload, File, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
type UploadedFile = {
name: string;
size: number;
type: string;
};
type FileUploadZoneProps = {
onFilesSelected: (files: File[]) => void;
onClose?: () => void;
};
export function FileUploadZone({ onFilesSelected, onClose }: FileUploadZoneProps) {
const [dragActive, setDragActive] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<UploadedFile[]>([]);
const handleDrag = useCallback((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 = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
}, []);
const handleFiles = (files: File[]) => {
const uploadedFiles = files.map(f => ({
name: f.name,
size: f.size,
type: f.type
}));
setSelectedFiles(prev => [...prev, ...uploadedFiles]);
onFilesSelected(files);
};
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
handleFiles(Array.from(e.target.files));
}
};
const removeFile = (index: number) => {
setSelectedFiles(prev => prev.filter((_, i) => i !== index));
};
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
};
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Upload Files</h3>
{onClose && (
<Button variant="ghost" size="icon" onClick={onClose} data-testid="button-close-upload">
<X className="h-5 w-5" />
</Button>
)}
</div>
<div
className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
dragActive ? "border-primary bg-primary/5" : "border-border"
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
data-testid="dropzone"
>
<Upload className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-sm font-medium mb-2">Drag and drop files here</p>
<p className="text-xs text-muted-foreground mb-4">or</p>
<label htmlFor="file-upload">
<Button variant="outline" asChild data-testid="button-browse-files">
<span className="cursor-pointer">Browse Files</span>
</Button>
</label>
<input
id="file-upload"
type="file"
multiple
onChange={handleFileInput}
className="hidden"
/>
<p className="text-xs text-muted-foreground mt-4">
Supports: .html, .js, .css, .py, .json, and more
</p>
</div>
{selectedFiles.length > 0 && (
<div className="mt-4 space-y-2">
<p className="text-sm font-medium">Selected Files:</p>
{selectedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-muted rounded-lg"
data-testid={`file-item-${index}`}
>
<div className="flex items-center gap-3">
<File className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">{file.name}</p>
<p className="text-xs text-muted-foreground">{formatFileSize(file.size)}</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => removeFile(index)}
data-testid={`button-remove-file-${index}`}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</Card>
);
}