102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
|
|
import React, { useCallback, useState } from 'react';
|
||
|
|
import { UploadCloud } from 'lucide-react';
|
||
|
|
import { useToast } from '../../context/ToastContext';
|
||
|
|
|
||
|
|
interface FileUploadZoneProps {
|
||
|
|
onFilesSelected: (files: File[]) => void;
|
||
|
|
acceptedFormats?: string[];
|
||
|
|
maxSizeInMB?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const FileUploadZone: React.FC<FileUploadZoneProps> = ({
|
||
|
|
onFilesSelected,
|
||
|
|
acceptedFormats = ['.wav', '.mp3', '.aiff', '.flac', '.zip'],
|
||
|
|
maxSizeInMB = 500
|
||
|
|
}) => {
|
||
|
|
const { addToast } = useToast();
|
||
|
|
const [isDragging, setIsDragging] = useState(false);
|
||
|
|
|
||
|
|
const validateFile = (file: File): boolean => {
|
||
|
|
// Check size
|
||
|
|
if (file.size > maxSizeInMB * 1024 * 1024) {
|
||
|
|
addToast(`File ${file.name} exceeds ${maxSizeInMB}MB limit`, 'error');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
// Check extension (simple check)
|
||
|
|
const ext = `.${ file.name.split('.').pop()?.toLowerCase()}`;
|
||
|
|
if (!acceptedFormats.includes(ext)) {
|
||
|
|
addToast(`Format ${ext} not supported for ${file.name}`, 'error');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
setIsDragging(false);
|
||
|
|
|
||
|
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
||
|
|
const validFiles = droppedFiles.filter(validateFile);
|
||
|
|
|
||
|
|
if (validFiles.length > 0) {
|
||
|
|
onFilesSelected(validFiles);
|
||
|
|
}
|
||
|
|
}, [onFilesSelected]);
|
||
|
|
|
||
|
|
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
if (e.target.files) {
|
||
|
|
const selectedFiles = Array.from(e.target.files);
|
||
|
|
const validFiles = selectedFiles.filter(validateFile);
|
||
|
|
if (validFiles.length > 0) {
|
||
|
|
onFilesSelected(validFiles);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
||
|
|
onDragLeave={() => setIsDragging(false)}
|
||
|
|
onDrop={handleDrop}
|
||
|
|
className={`
|
||
|
|
border-2 border-dashed rounded-xl p-10 flex flex-col items-center justify-center text-center transition-all duration-300 group cursor-pointer
|
||
|
|
${isDragging
|
||
|
|
? 'border-kodo-cyan bg-kodo-cyan/10 scale-[1.02]'
|
||
|
|
: 'border-kodo-steel/50 bg-kodo-ink/30 hover:bg-kodo-ink/50 hover:border-kodo-cyan/50'}
|
||
|
|
`}
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="file"
|
||
|
|
multiple
|
||
|
|
className="hidden"
|
||
|
|
id="file-upload-input"
|
||
|
|
onChange={handleFileInput}
|
||
|
|
accept={acceptedFormats.join(',')}
|
||
|
|
/>
|
||
|
|
<label htmlFor="file-upload-input" className="cursor-pointer w-full flex flex-col items-center">
|
||
|
|
<div className={`
|
||
|
|
w-20 h-20 rounded-full flex items-center justify-center mb-6 transition-all duration-300 shadow-lg
|
||
|
|
${isDragging ? 'bg-kodo-cyan text-black scale-110' : 'bg-kodo-slate text-kodo-cyan group-hover:scale-110 group-hover:bg-kodo-steel'}
|
||
|
|
`}>
|
||
|
|
<UploadCloud className="w-10 h-10" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h3 className="text-2xl font-display font-bold text-white mb-2">
|
||
|
|
{isDragging ? 'Drop Files Here' : 'Drag & Drop or Click'}
|
||
|
|
</h3>
|
||
|
|
<p className="text-gray-400 mb-6 max-w-sm">
|
||
|
|
Upload audio stems, project archives, or sample packs.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div className="flex flex-wrap justify-center gap-2 max-w-md">
|
||
|
|
{acceptedFormats.map(fmt => (
|
||
|
|
<span key={fmt} className="px-2 py-1 bg-black/30 rounded text-[10px] font-mono text-gray-500 border border-white/5 uppercase">
|
||
|
|
{fmt.replace('.', '')}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<p className="mt-4 text-[10px] text-gray-600">Max file size: {maxSizeInMB}MB</p>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|