'use client'; import * as React from 'react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; interface ImageFile { file: File; preview: string; } interface ImageUploadProps { images: ImageFile[]; onChange: (images: ImageFile[]) => void; maxFiles?: number; className?: string; } export function ImageUpload({ images, onChange, maxFiles = 20, className }: ImageUploadProps) { const inputRef = React.useRef(null); const [isDragging, setIsDragging] = React.useState(false); const addFiles = React.useCallback( (files: FileList | File[]) => { const newImages: ImageFile[] = []; const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; const maxSize = 10 * 1024 * 1024; // 10MB Array.from(files).forEach((file) => { if (!allowedTypes.includes(file.type)) return; if (file.size > maxSize) return; if (images.length + newImages.length >= maxFiles) return; newImages.push({ file, preview: URL.createObjectURL(file), }); }); if (newImages.length > 0) { onChange([...images, ...newImages]); } }, [images, onChange, maxFiles], ); const removeImage = React.useCallback( (index: number) => { const updated = [...images]; URL.revokeObjectURL(updated[index]!.preview); updated.splice(index, 1); onChange(updated); }, [images, onChange], ); const handleDragOver = React.useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = React.useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleDrop = React.useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files.length > 0) { addFiles(e.dataTransfer.files); } }, [addFiles], ); React.useEffect(() => { return () => { images.forEach((img) => URL.revokeObjectURL(img.preview)); }; }, []); // intentionally empty: runs only on unmount to revoke object URLs return (
inputRef.current?.click()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); inputRef.current?.click(); } }} aria-label="Tải ảnh lên. Kéo thả ảnh vào đây hoặc nhấn để chọn" className={cn( 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25 hover:border-primary/50', )} >

Kéo thả ảnh vào đây hoặc nhấp để chọn

JPG, PNG, WebP - Tối đa {maxFiles} ảnh, mỗi ảnh 10MB

{ if (e.target.files) addFiles(e.target.files); e.target.value = ''; }} />
{images.length > 0 && (
{images.map((img, index) => (
{`Ảnh
{index === 0 && ( Ảnh bìa )}
))}
)}
); } export type { ImageFile };