From 84e9b2beb72ee1c5e3b1cc361664e48db2c821b1 Mon Sep 17 00:00:00 2001 From: MehrdadAdabi <126083584+mehrdadAdabi@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:12:09 +0330 Subject: [PATCH] create file-uploader --- src/core/components/base/file-uploader.tsx | 229 +++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/core/components/base/file-uploader.tsx diff --git a/src/core/components/base/file-uploader.tsx b/src/core/components/base/file-uploader.tsx new file mode 100644 index 0000000..120bf1d --- /dev/null +++ b/src/core/components/base/file-uploader.tsx @@ -0,0 +1,229 @@ +import { cn } from "@/core/lib/utils"; +import { File, Upload, X } from "lucide-react"; +import { type ChangeEvent, useRef, useState } from "react"; + +type FileUploaderProps = { + label?: string; + onFileChange: (file: File | null) => void; + onRemove?: () => void; + className?: string; + error?: string; + required?: boolean; + accept?: string; + maxSize?: number; // in MB + multiple?: boolean; + disabled?: boolean; + helperText?: string; + showPreview?: boolean; +}; + +export function FileUploader({ + label = "آپلود فایل", + onFileChange, + onRemove, + className, + error, + required = false, + accept, + maxSize = 10, + multiple = false, + disabled = false, + helperText, + showPreview = true, +}: FileUploaderProps) { + const fileInputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [fileError, setFileError] = useState(""); + + const handleFileChange = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + validateAndSetFile(file); + } + }; + + const validateAndSetFile = (file: File) => { + setFileError(""); + + // Check file size + if (file.size > maxSize * 1024 * 1024) { + setFileError(`حجم فایل نباید بیشتر از ${maxSize} مگابایت باشد`); + return; + } + + // Check file type if accept is specified + if (accept) { + const acceptedTypes = accept.split(",").map((type) => type.trim()); + const fileExtension = `.${file.name.split(".").pop()?.toLowerCase()}`; + const fileMimeType = file.type; + + const isAccepted = acceptedTypes.some((type) => { + if (type.startsWith(".")) { + return fileExtension === type.toLowerCase(); + } + if (type.endsWith("/*")) { + const category = type.split("/")[0]; + return fileMimeType.startsWith(category); + } + return fileMimeType === type; + }); + + if (!isAccepted) { + setFileError("نوع فایل انتخاب شده مجاز نیست"); + return; + } + } + + setSelectedFile(file); + onFileChange(file); + }; + + const handleRemove = () => { + setSelectedFile(null); + setFileError(""); + onFileChange(null); + onRemove?.(); + if (fileInputRef.current) fileInputRef.current.value = ""; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + if (!disabled) { + setIsDragging(true); + } + }; + + const handleDragLeave = () => { + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + if (disabled) return; + + const file = e.dataTransfer.files?.[0]; + if (file) { + validateAndSetFile(file); + } + }; + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i]; + }; + + const getAcceptText = (): string => { + if (!accept) return `تا ${maxSize} مگابایت`; + + const types = accept.split(",").map((type) => type.trim()); + const extensions = types + .filter((type) => type.startsWith(".")) + .map((type) => type.toUpperCase()) + .join(", "); + + return extensions ? `${extensions} تا ${maxSize} مگابایت` : `تا ${maxSize} مگابایت`; + }; + + return ( +
+ {label && ( + + )} + + {selectedFile && showPreview ? ( +
+
+
+
+ +
+
+

+ {selectedFile.name} +

+

+ {formatFileSize(selectedFile.size)} +

+
+
+ +
+ {!disabled && ( + + )} +
+ ) : ( + + )} + + {(error || fileError) && ( +

+ {error || fileError} +

+ )} +
+ ); +}