"use client" import * as React from "react" import { ImageIcon, XIcon } from "lucide-react" import { FileUpload, formatBytes, getFileKey, type FileUploadProps, type FileUploadRenderFileState, } from "@/components/upload/file-upload" import { Button } from "@/components/ui/button" import { cn, stopInteractivePropagation } from "@/lib/utils" export type ImageUploadProps = Omit & { accept?: string preview?: boolean previewClassName?: string imageClassName?: string minImageWidth?: number minImageHeight?: number aspectRatio?: number aspectRatioTolerance?: number renderImageFile?: (state: FileUploadRenderFileState & { previewUrl?: string }) => React.ReactNode } function readImageMeta(file: File) { return new Promise<{ width: number; height: number }>((resolve, reject) => { const url = URL.createObjectURL(file) const image = new Image() image.onload = () => { resolve({ width: image.width, height: image.height }) URL.revokeObjectURL(url) } image.onerror = () => { URL.revokeObjectURL(url) reject(new Error("Image metadata could not be read")) } image.src = url }) } function useImagePreviewUrls(files: File[], enabled = true) { const [urls, setUrls] = React.useState>({}) React.useEffect(() => { if (!enabled) { setUrls({}) return } const entries = files .filter((file) => file.type.startsWith("image/")) .map((file) => [getFileKey(file), URL.createObjectURL(file)] as const) setUrls(Object.fromEntries(entries)) return () => { entries.forEach(([, url]) => URL.revokeObjectURL(url)) } }, [enabled, files]) return urls } function defaultRenderImageFile({ file, previewUrl, progress, remove, previewClassName, imageClassName, }: FileUploadRenderFileState & { previewUrl?: string previewClassName?: string imageClassName?: string }) { return (
{previewUrl ? ( {file.name} ) : ( )}
{file.name}
{formatBytes(file.size)}
{typeof progress === "number" && (
)}
) } function ImageUpload({ accept = "image/*", buttonLabel = "Choose image", dropzoneLabel = "Drop images here or click to upload", preview = true, previewClassName, imageClassName, files = [], minImageWidth, minImageHeight, aspectRatio, aspectRatioTolerance = 0.04, renderImageFile, ...props }: ImageUploadProps) { const previewUrls = useImagePreviewUrls(files, preview) return ( { if (!file.type.startsWith("image/")) return "Only image files are allowed." if (!minImageWidth && !minImageHeight && !aspectRatio) return null const { width, height } = await readImageMeta(file) if (minImageWidth && width < minImageWidth) { return `Image width must be at least ${minImageWidth}px.` } if (minImageHeight && height < minImageHeight) { return `Image height must be at least ${minImageHeight}px.` } if (aspectRatio) { const ratio = width / height if (Math.abs(ratio - aspectRatio) > aspectRatioTolerance) { return `Image ratio must be close to ${aspectRatio.toFixed(2)}:1.` } } return null }} renderFile={(state) => { const previewUrl = previewUrls[getFileKey(state.file)] return renderImageFile?.({ ...state, previewUrl }) ?? defaultRenderImageFile({ ...state, previewUrl, previewClassName, imageClassName }) }} {...props} /> ) } export { ImageUpload, useImagePreviewUrls }