import { useCallback, useRef, useState, type ReactNode } from 'react'; import { Upload, X } from 'lucide-react'; import { cn } from '../utils/cn'; /** * Generic file dropzone with a react-dropzone-compatible API surface. * Implemented on top of the native HTML5 drag-and-drop API so no extra dep is required. * * Differences vs react-dropzone: * - `accept` is a comma-separated MIME string (browser-native form), not the object form * - No fancy filesystem-access fallback; relies on */ export interface FileDropError { code: 'file-too-large' | 'too-many-files' | 'invalid-type'; message: string; file?: File; } interface Props { onFiles: (files: File[]) => void; onError?: (error: FileDropError) => void; /** Comma-separated accept string, e.g. `"image/jpeg,image/png,image/webp"` */ accept?: string; /** Max bytes per file. Default 12 MB. */ maxSize?: number; maxFiles?: number; multiple?: boolean; disabled?: boolean; className?: string; hint?: string; children?: ReactNode; } const DEFAULT_ACCEPT = 'image/jpeg,image/png,image/webp'; const DEFAULT_MAX_SIZE = 12 * 1024 * 1024; // 12 MB const DEFAULT_MAX_FILES = 10; export function FileDrop({ onFiles, onError, accept = DEFAULT_ACCEPT, maxSize = DEFAULT_MAX_SIZE, maxFiles = DEFAULT_MAX_FILES, multiple = true, disabled, className, hint, children, }: Props) { const inputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [error, setError] = useState(null); const acceptList = accept .split(',') .map((s) => s.trim()) .filter(Boolean); const matchesAccept = useCallback( (file: File): boolean => { if (acceptList.length === 0) return true; return acceptList.some((rule) => { if (rule.startsWith('.')) return file.name.toLowerCase().endsWith(rule.toLowerCase()); if (rule.endsWith('/*')) { const prefix = rule.slice(0, -1); // keep trailing slash return file.type.startsWith(prefix); } return file.type === rule; }); }, [acceptList], ); const validate = useCallback( (files: File[]): { ok: File[]; error: FileDropError | null } => { if (files.length > maxFiles) { return { ok: [], error: { code: 'too-many-files', message: `En fazla ${maxFiles} dosya yükleyebilirsin.`, }, }; } for (const f of files) { if (!matchesAccept(f)) { return { ok: [], error: { code: 'invalid-type', message: `Desteklenmeyen dosya türü: ${f.name}`, file: f, }, }; } if (f.size > maxSize) { const mb = Math.round((maxSize / (1024 * 1024)) * 10) / 10; return { ok: [], error: { code: 'file-too-large', message: `Dosya çok büyük: ${f.name} (>${mb}MB)`, file: f, }, }; } } return { ok: files, error: null }; }, [maxFiles, matchesAccept, maxSize], ); const handleFiles = useCallback( (list: FileList | null) => { if (!list || list.length === 0) return; const files = Array.from(list); const { ok, error: err } = validate(files); if (err) { setError(err.message); onError?.(err); return; } setError(null); onFiles(ok); }, [onFiles, onError, validate], ); return (
handleFiles(e.target.files)} /> {error && (
{error}
)}
); }