Files
thrilltrack-explorer/src-old/components/upload/DragDropZone.tsx

199 lines
5.3 KiB
TypeScript

import React, { useCallback, useState } from 'react';
import { Upload, Image, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useToast } from '@/hooks/use-toast';
interface DragDropZoneProps {
onFilesAdded: (files: File[]) => void;
maxFiles?: number;
maxSizeMB?: number;
allowedFileTypes?: string[];
disabled?: boolean;
className?: string;
children?: React.ReactNode;
}
export function DragDropZone({
onFilesAdded,
maxFiles = 10,
maxSizeMB = 25,
allowedFileTypes = ['image/*'],
disabled = false,
className = '',
children,
}: DragDropZoneProps) {
const [isDragOver, setIsDragOver] = useState(false);
const { toast } = useToast();
const validateFiles = useCallback((files: FileList) => {
const validFiles: File[] = [];
const errors: string[] = [];
Array.from(files).forEach((file) => {
// Check file type
const isValidType = allowedFileTypes.some(type => {
if (type === 'image/*') {
return file.type.startsWith('image/');
}
return file.type === type;
});
if (!isValidType) {
errors.push(`${file.name}: Invalid file type`);
return;
}
// Check file size
if (file.size > maxSizeMB * 1024 * 1024) {
errors.push(`${file.name}: File too large (max ${maxSizeMB}MB)`);
return;
}
validFiles.push(file);
});
// Check total file count
if (validFiles.length > maxFiles) {
errors.push(`Too many files. Maximum ${maxFiles} files allowed.`);
return { validFiles: validFiles.slice(0, maxFiles), errors };
}
return { validFiles, errors };
}, [allowedFileTypes, maxSizeMB, maxFiles]);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!disabled) {
setIsDragOver(true);
}
}, [disabled]);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (disabled) return;
const files = e.dataTransfer.files;
if (files.length === 0) return;
const { validFiles, errors } = validateFiles(files);
if (errors.length > 0) {
toast({
variant: 'destructive',
title: 'File Validation Error',
description: errors.join(', '),
});
}
if (validFiles.length > 0) {
onFilesAdded(validFiles);
}
}, [disabled, validateFiles, onFilesAdded, toast]);
const handleFileInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) return;
const files = e.target.files;
if (!files || files.length === 0) return;
const { validFiles, errors } = validateFiles(files);
if (errors.length > 0) {
toast({
variant: 'destructive',
title: 'File Validation Error',
description: errors.join(', '),
});
}
if (validFiles.length > 0) {
onFilesAdded(validFiles);
}
// Reset input
e.target.value = '';
}, [disabled, validateFiles, onFilesAdded, toast]);
if (children) {
return (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"relative transition-all duration-200",
isDragOver && !disabled && "ring-2 ring-accent ring-offset-2",
className
)}
>
<input
type="file"
multiple
accept={allowedFileTypes.join(',')}
onChange={handleFileInput}
disabled={disabled}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
{children}
</div>
);
}
return (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"relative border-2 border-dashed rounded-lg transition-all duration-200 p-8",
isDragOver && !disabled
? "border-accent bg-accent/5 scale-[1.02]"
: "border-border hover:border-accent/50",
disabled && "opacity-50 cursor-not-allowed",
!disabled && "cursor-pointer hover:bg-muted/50",
className
)}
>
<input
type="file"
multiple
accept={allowedFileTypes.join(',')}
onChange={handleFileInput}
disabled={disabled}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<div className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-accent/10 rounded-full flex items-center justify-center">
{isDragOver ? (
<Upload className="w-8 h-8 text-accent animate-bounce" />
) : (
<Image className="w-8 h-8 text-accent" />
)}
</div>
<div className="space-y-2">
<p className="text-lg font-medium">
{isDragOver ? 'Drop files here' : 'Drag & drop photos here'}
</p>
<p className="text-sm text-muted-foreground">
or click to browse files
</p>
<p className="text-xs text-muted-foreground">
Max {maxFiles} files, {maxSizeMB}MB each
</p>
</div>
</div>
</div>
);
}