From 6bcdf7251291dd4347231d202ef7929f4af76231 Mon Sep 17 00:00:00 2001 From: hossein taromi Date: Sun, 27 Jul 2025 14:45:17 +0330 Subject: [PATCH] feat(ui): add reusable UI components for product management - Add MultiSelectAutocomplete for multi-selection with search - Add TagInput for managing array of string values - Add FileUploader with drag & drop and progress tracking - Add VariantManager for comprehensive product variant management - Add useFileUpload hooks for file upload/delete operations --- src/components/ui/FileUploader.tsx | 325 ++++++++++ src/components/ui/MultiSelectAutocomplete.tsx | 186 ++++++ src/components/ui/TagInput.tsx | 111 ++++ src/components/ui/VariantManager.tsx | 567 ++++++++++++++++++ src/hooks/useFileUpload.ts | 71 +++ 5 files changed, 1260 insertions(+) create mode 100644 src/components/ui/FileUploader.tsx create mode 100644 src/components/ui/MultiSelectAutocomplete.tsx create mode 100644 src/components/ui/TagInput.tsx create mode 100644 src/components/ui/VariantManager.tsx create mode 100644 src/hooks/useFileUpload.ts diff --git a/src/components/ui/FileUploader.tsx b/src/components/ui/FileUploader.tsx new file mode 100644 index 0000000..e43ccd5 --- /dev/null +++ b/src/components/ui/FileUploader.tsx @@ -0,0 +1,325 @@ +import React, { useState, useRef, useCallback } from 'react'; +import { Upload, X, Image, File, AlertCircle, CheckCircle } from 'lucide-react'; +import { Button } from './Button'; + +export interface UploadedFile { + id: string; + name: string; + size: number; + type: string; + url?: string; + preview?: string; + progress: number; + status: 'uploading' | 'completed' | 'error'; + error?: string; +} + +interface FileUploaderProps { + onUpload: (file: File) => Promise<{ id: string; url: string }>; + onRemove?: (fileId: string) => void; + acceptedTypes?: string[]; + maxFileSize?: number; // در بایت + maxFiles?: number; + label?: string; + description?: string; + error?: string; + disabled?: boolean; + className?: string; +} + +export const FileUploader: React.FC = ({ + onUpload, + onRemove, + acceptedTypes = ['image/*', 'video/*'], + maxFileSize = 10 * 1024 * 1024, // 10MB + maxFiles = 10, + label = "فایل‌ها", + description = "تصاویر و ویدیوها را اینجا بکشید یا کلیک کنید", + error, + disabled = false, + className = "", +}) => { + const [files, setFiles] = useState([]); + const [isDragOver, setIsDragOver] = useState(false); + const fileInputRef = useRef(null); + + const isImage = (type: string) => type.startsWith('image/'); + + const formatFileSize = (bytes: number) => { + 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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const validateFile = (file: File) => { + if (maxFileSize && file.size > maxFileSize) { + return `حجم فایل نباید بیشتر از ${formatFileSize(maxFileSize)} باشد`; + } + + if (acceptedTypes.length > 0) { + const isAccepted = acceptedTypes.some(type => { + if (type === 'image/*') return file.type.startsWith('image/'); + if (type === 'video/*') return file.type.startsWith('video/'); + return file.type === type; + }); + if (!isAccepted) { + return 'نوع فایل پشتیبانی نمی‌شود'; + } + } + + if (maxFiles && files.length >= maxFiles) { + return `حداکثر ${maxFiles} فایل مجاز است`; + } + + return null; + }; + + const createFilePreview = (file: File) => { + return new Promise((resolve) => { + if (isImage(file.type)) { + const reader = new FileReader(); + reader.onload = (e) => resolve(e.target?.result as string); + reader.readAsDataURL(file); + } else { + resolve(''); + } + }); + }; + + const handleFileUpload = useCallback(async (file: File) => { + const validationError = validateFile(file); + if (validationError) { + const errorFile: UploadedFile = { + id: Math.random().toString(36).substr(2, 9), + name: file.name, + size: file.size, + type: file.type, + progress: 0, + status: 'error', + error: validationError, + }; + setFiles(prev => [...prev, errorFile]); + return; + } + + const fileId = Math.random().toString(36).substr(2, 9); + const preview = await createFilePreview(file); + + const newFile: UploadedFile = { + id: fileId, + name: file.name, + size: file.size, + type: file.type, + preview, + progress: 0, + status: 'uploading', + }; + + setFiles(prev => [...prev, newFile]); + + try { + // شبیه‌سازی پروگرس + const progressInterval = setInterval(() => { + setFiles(prev => prev.map(f => + f.id === fileId && f.progress < 90 + ? { ...f, progress: f.progress + 10 } + : f + )); + }, 200); + + const result = await onUpload(file); + + clearInterval(progressInterval); + + setFiles(prev => prev.map(f => + f.id === fileId + ? { ...f, progress: 100, status: 'completed', url: result.url, id: result.id } + : f + )); + } catch (error: any) { + setFiles(prev => prev.map(f => + f.id === fileId + ? { ...f, status: 'error', error: error.message || 'خطا در آپلود فایل' } + : f + )); + } + }, [onUpload, files.length, maxFiles, maxFileSize, acceptedTypes]); + + const handleFileSelect = (selectedFiles: FileList) => { + Array.from(selectedFiles).forEach(file => { + handleFileUpload(file); + }); + }; + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + if (disabled) return; + + const droppedFiles = e.dataTransfer.files; + handleFileSelect(droppedFiles); + }, [disabled, handleFileUpload]); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + if (!disabled) setIsDragOver(true); + }, [disabled]); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + }, []); + + const handleClick = () => { + if (!disabled) fileInputRef.current?.click(); + }; + + const handleRemove = (fileId: string) => { + setFiles(prev => prev.filter(f => f.id !== fileId)); + onRemove?.(fileId); + }; + + return ( +
+ {label && ( + + )} + + {/* Upload Area */} +
+ e.target.files && handleFileSelect(e.target.files)} + disabled={disabled} + /> + +
+ +
+

+ {description} +

+

+ حداکثر {formatFileSize(maxFileSize)} • {acceptedTypes.join(', ')} +

+
+
+
+ + {error && ( +

+ + {error} +

+ )} + + {/* Files List */} + {files.length > 0 && ( +
+

+ فایل‌های آپلود شده ({files.length}) +

+
+ {files.map((file) => ( +
+ {/* File Icon/Preview */} +
+ {file.preview ? ( + {file.name} + ) : ( +
+ {isImage(file.type) ? ( + + ) : ( + + )} +
+ )} +
+ + {/* File Info */} +
+

+ {file.name} +

+

+ {formatFileSize(file.size)} +

+ + {/* Progress Bar */} + {file.status === 'uploading' && ( +
+
+
+
+

{file.progress}%

+
+ )} + + {/* Error Message */} + {file.status === 'error' && file.error && ( +

+ + {file.error} +

+ )} +
+ + {/* Status & Actions */} +
+ {file.status === 'completed' && ( + + )} + {file.status === 'error' && ( + + )} + + +
+
+ ))} +
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/MultiSelectAutocomplete.tsx b/src/components/ui/MultiSelectAutocomplete.tsx new file mode 100644 index 0000000..a1c9250 --- /dev/null +++ b/src/components/ui/MultiSelectAutocomplete.tsx @@ -0,0 +1,186 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { ChevronDown, X, Search } from 'lucide-react'; + +export interface Option { + id: number; + title: string; + description?: string; +} + +interface MultiSelectAutocompleteProps { + options: Option[]; + selectedValues: number[]; + onChange: (values: number[]) => void; + placeholder?: string; + label?: string; + error?: string; + isLoading?: boolean; + disabled?: boolean; +} + +export const MultiSelectAutocomplete: React.FC = ({ + options, + selectedValues, + onChange, + placeholder = "انتخاب کنید...", + label, + error, + isLoading = false, + disabled = false, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + + const filteredOptions = options.filter(option => + option.title.toLowerCase().includes(searchTerm.toLowerCase()) || + (option.description && option.description.toLowerCase().includes(searchTerm.toLowerCase())) + ); + + const selectedOptions = options.filter(option => selectedValues.includes(option.id)); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + setSearchTerm(''); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleToggleOption = (optionId: number) => { + if (selectedValues.includes(optionId)) { + onChange(selectedValues.filter(id => id !== optionId)); + } else { + onChange([...selectedValues, optionId]); + } + }; + + const handleRemoveOption = (optionId: number) => { + onChange(selectedValues.filter(id => id !== optionId)); + }; + + const handleToggleDropdown = () => { + if (disabled) return; + setIsOpen(!isOpen); + if (!isOpen) { + setTimeout(() => inputRef.current?.focus(), 100); + } + }; + + return ( +
+ {label && ( + + )} + + {/* Selected Items Display */} +
+
+ {selectedOptions.length > 0 ? ( + selectedOptions.map(option => ( + + {option.title} + + + )) + ) : ( + {placeholder} + )} + +
+ {isOpen && !disabled && ( + setSearchTerm(e.target.value)} + className="w-full border-none outline-none bg-transparent text-sm" + placeholder="جستجو..." + /> + )} +
+ + +
+
+ + {/* Dropdown */} + {isOpen && !disabled && ( +
+ {isLoading ? ( +
+ در حال بارگذاری... +
+ ) : filteredOptions.length > 0 ? ( + filteredOptions.map(option => ( +
handleToggleOption(option.id)} + > +
+
+
+ {option.title} +
+ {option.description && ( +
+ {option.description} +
+ )} +
+ {selectedValues.includes(option.id) && ( +
+ )} +
+
+ )) + ) : ( +
+ موردی یافت نشد +
+ )} +
+ )} + + {error && ( +

{error}

+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/TagInput.tsx b/src/components/ui/TagInput.tsx new file mode 100644 index 0000000..ee74737 --- /dev/null +++ b/src/components/ui/TagInput.tsx @@ -0,0 +1,111 @@ +import React, { useState, KeyboardEvent } from 'react'; +import { X } from 'lucide-react'; + +interface TagInputProps { + values: string[]; + onChange: (values: string[]) => void; + placeholder?: string; + label?: string; + error?: string; + disabled?: boolean; +} + +export const TagInput: React.FC = ({ + values, + onChange, + placeholder = "اضافه کنید و Enter بزنید...", + label, + error, + disabled = false, +}) => { + const [inputValue, setInputValue] = useState(''); + + const addValue = (value: string) => { + const trimmedValue = value.trim(); + if (trimmedValue && !values.includes(trimmedValue)) { + onChange([...values, trimmedValue]); + setInputValue(''); + } + }; + + const removeValue = (index: number) => { + const newValues = values.filter((_, i) => i !== index); + onChange(newValues); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + addValue(inputValue); + } else if (e.key === 'Backspace' && !inputValue && values.length > 0) { + removeValue(values.length - 1); + } + }; + + const handleInputBlur = () => { + if (inputValue.trim()) { + addValue(inputValue); + } + }; + + return ( +
+ {label && ( + + )} + +
+
+ {values.map((value, index) => ( + + {value} + {!disabled && ( + + )} + + ))} + + {!disabled && ( + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleInputBlur} + placeholder={values.length === 0 ? placeholder : ""} + className="flex-1 min-w-[120px] border-none outline-none bg-transparent text-sm dark:text-gray-100" + /> + )} +
+
+ + {error && ( +

{error}

+ )} + + {!disabled && ( +

+ Enter بزنید یا روی جای دیگری کلیک کنید تا مقدار اضافه شود +

+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/VariantManager.tsx b/src/components/ui/VariantManager.tsx new file mode 100644 index 0000000..99224e0 --- /dev/null +++ b/src/components/ui/VariantManager.tsx @@ -0,0 +1,567 @@ +import React, { useState } from 'react'; +import { Plus, Trash2, Edit3, Package } from 'lucide-react'; +import { ProductVariantFormData, ProductImage } from '../../pages/products/core/_models'; +import { Button } from './Button'; +import { Input } from './Input'; +import { FileUploader } from './FileUploader'; +import { useFileUpload, useFileDelete } from '../../hooks/useFileUpload'; + +interface VariantManagerProps { + variants: ProductVariantFormData[]; + onChange: (variants: ProductVariantFormData[]) => void; + disabled?: boolean; +} + +interface VariantFormProps { + variant?: ProductVariantFormData; + onSave: (variant: ProductVariantFormData) => void; + onCancel: () => void; + isEdit?: boolean; +} + +const VariantForm: React.FC = ({ variant, onSave, onCancel, isEdit = false }) => { + const [formData, setFormData] = useState( + variant || { + enabled: true, + fee_percentage: 0, + profit_percentage: 0, + stock_limit: 0, + stock_managed: true, + stock_number: 0, + weight: 0, + attributes: {}, + meta: {}, + images: [] + } + ); + + const [uploadedImages, setUploadedImages] = useState(variant?.images || []); + const [attributes, setAttributes] = useState>(variant?.attributes || {}); + const [meta, setMeta] = useState>(variant?.meta || {}); + const [newAttributeKey, setNewAttributeKey] = useState(''); + const [newAttributeValue, setNewAttributeValue] = useState(''); + const [newMetaKey, setNewMetaKey] = useState(''); + const [newMetaValue, setNewMetaValue] = useState(''); + + const { mutateAsync: uploadFile } = useFileUpload(); + const { mutate: deleteFile } = useFileDelete(); + + const handleInputChange = (field: keyof ProductVariantFormData, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const handleFileUpload = async (file: File) => { + try { + const result = await uploadFile(file); + const newImage: ProductImage = { + id: result.id, + url: result.url, + alt: file.name, + order: uploadedImages.length + }; + + const updatedImages = [...uploadedImages, newImage]; + setUploadedImages(updatedImages); + + return result; + } catch (error) { + console.error('Upload error:', error); + throw error; + } + }; + + const handleFileRemove = (fileId: string) => { + const updatedImages = uploadedImages.filter(img => img.id !== fileId); + setUploadedImages(updatedImages); + deleteFile(fileId); + }; + + const handleAddAttribute = () => { + if (newAttributeKey.trim() && newAttributeValue.trim()) { + const updatedAttributes = { + ...attributes, + [newAttributeKey.trim()]: newAttributeValue.trim() + }; + setAttributes(updatedAttributes); + setNewAttributeKey(''); + setNewAttributeValue(''); + } + }; + + const handleRemoveAttribute = (key: string) => { + const updatedAttributes = { ...attributes }; + delete updatedAttributes[key]; + setAttributes(updatedAttributes); + }; + + const handleAddMeta = () => { + if (newMetaKey.trim() && newMetaValue.trim()) { + const updatedMeta = { + ...meta, + [newMetaKey.trim()]: newMetaValue.trim() + }; + setMeta(updatedMeta); + setNewMetaKey(''); + setNewMetaValue(''); + } + }; + + const handleRemoveMeta = (key: string) => { + const updatedMeta = { ...meta }; + delete updatedMeta[key]; + setMeta(updatedMeta); + }; + + const handleSave = () => { + const variantToSave: ProductVariantFormData = { + ...formData, + images: uploadedImages, + attributes, + meta + }; + onSave(variantToSave); + }; + + return ( +
+
+

+ {isEdit ? 'ویرایش Variant' : 'افزودن Variant جدید'} +

+
+ + +
+
+ + {/* Basic Info */} +
+
+ + handleInputChange('fee_percentage', parseFloat(e.target.value) || 0)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + placeholder="0" + min="0" + max="100" + step="0.1" + /> +
+ +
+ + handleInputChange('profit_percentage', parseFloat(e.target.value) || 0)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + placeholder="0" + min="0" + max="100" + step="0.1" + /> +
+ +
+ + handleInputChange('weight', parseFloat(e.target.value) || 0)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + placeholder="0" + min="0" + step="0.1" + /> +
+
+ + {/* Stock Management */} +
+
+ مدیریت موجودی +
+
+
+ handleInputChange('stock_managed', e.target.checked)} + className="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500" + /> + +
+ +
+ + handleInputChange('stock_number', parseInt(e.target.value) || 0)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + placeholder="0" + min="0" + disabled={!formData.stock_managed} + /> +
+ +
+ + handleInputChange('stock_limit', parseInt(e.target.value) || 0)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + placeholder="0" + min="0" + disabled={!formData.stock_managed} + /> +
+
+
+ + {/* Images */} +
+
+ تصاویر Variant +
+ + + {uploadedImages.length > 0 && ( +
+
+ {uploadedImages.map((image, index) => ( +
+ {image.alt + +
+ ))} +
+
+ )} +
+ + {/* Attributes */} +
+
+ ویژگی‌های Variant +
+ +
+ setNewAttributeKey(e.target.value)} + placeholder="نام ویژگی (مثل: رنگ، سایز)" + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> + setNewAttributeValue(e.target.value)} + placeholder="مقدار (مثل: قرمز، بزرگ)" + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> + +
+ + {Object.keys(attributes).length > 0 && ( +
+ {Object.entries(attributes).map(([key, value]) => ( +
+ + {key}: {String(value)} + + +
+ ))} +
+ )} +
+ + {/* Meta Data */} +
+
+ Meta Data +
+ +
+ setNewMetaKey(e.target.value)} + placeholder="کلید Meta" + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> + setNewMetaValue(e.target.value)} + placeholder="مقدار Meta" + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> + +
+ + {Object.keys(meta).length > 0 && ( +
+ {Object.entries(meta).map(([key, value]) => ( +
+ + {key}: {String(value)} + + +
+ ))} +
+ )} +
+ + {/* Status */} +
+ handleInputChange('enabled', e.target.checked)} + className="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500" + /> + +
+
+ ); +}; + +export const VariantManager: React.FC = ({ variants, onChange, disabled = false }) => { + const [showForm, setShowForm] = useState(false); + const [editingIndex, setEditingIndex] = useState(null); + + const handleAddVariant = () => { + setEditingIndex(null); + setShowForm(true); + }; + + const handleEditVariant = (index: number) => { + setEditingIndex(index); + setShowForm(true); + }; + + const handleDeleteVariant = (index: number) => { + const updatedVariants = variants.filter((_, i) => i !== index); + onChange(updatedVariants); + }; + + const handleSaveVariant = (variant: ProductVariantFormData) => { + if (editingIndex !== null) { + // Edit existing variant + const updatedVariants = [...variants]; + updatedVariants[editingIndex] = variant; + onChange(updatedVariants); + } else { + // Add new variant + onChange([...variants, variant]); + } + setShowForm(false); + setEditingIndex(null); + }; + + const handleCancelForm = () => { + setShowForm(false); + setEditingIndex(null); + }; + + return ( +
+
+

+ Variants محصول ({variants.length}) +

+ {!disabled && !showForm && ( + + )} +
+ + {/* Show Form */} + {showForm && ( + + )} + + {/* Variants List */} + {variants.length > 0 && ( +
+ {variants.map((variant, index) => ( +
+
+
+
+

+ Variant {index + 1} +

+ + {variant.enabled ? 'فعال' : 'غیرفعال'} + +
+ +
+
+ درصد کارمزد: {variant.fee_percentage}% +
+
+ درصد سود: {variant.profit_percentage}% +
+
+ موجودی: {variant.stock_managed ? `${variant.stock_number} عدد` : 'بدون محدودیت'} +
+
+ وزن: {variant.weight} گرم +
+
+ + {variant.images && variant.images.length > 0 && ( +
+ {variant.images.slice(0, 3).map((image, imgIndex) => ( + {image.alt + ))} + {variant.images.length > 3 && ( +
+ +{variant.images.length - 3} +
+ )} +
+ )} + + {/* Show Attributes if any */} + {Object.keys(variant.attributes).length > 0 && ( +
+
ویژگی‌ها:
+
+ {Object.entries(variant.attributes).map(([key, value]) => ( + + {key}: {String(value)} + + ))} +
+
+ )} +
+ + {!disabled && ( +
+ + +
+ )} +
+
+ ))} +
+ )} + + {variants.length === 0 && !showForm && ( +
+ +

+ هنوز هیچ Variant ای اضافه نشده +

+ {!disabled && ( + + )} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/hooks/useFileUpload.ts b/src/hooks/useFileUpload.ts new file mode 100644 index 0000000..23d60c5 --- /dev/null +++ b/src/hooks/useFileUpload.ts @@ -0,0 +1,71 @@ +import { useMutation } from "@tanstack/react-query"; +import { + httpPostRequest, + httpDeleteRequest, + APIUrlGenerator, +} from "@/utils/baseHttpService"; +import { API_ROUTES } from "@/constant/routes"; +import toast from "react-hot-toast"; + +interface UploadFileResponse { + id: string; + name: string; + size: number; + type: string; + url: string; + serve_key: string; + created_at: string; +} + +interface UploadResponse { + file: UploadFileResponse; +} + +export const useFileUpload = () => { + return useMutation({ + mutationFn: async (file: File): Promise<{ id: string; url: string }> => { + const formData = new FormData(); + formData.append("file", file); + + console.log("Uploading file:", file.name); + + const response = await httpPostRequest( + APIUrlGenerator(API_ROUTES.UPLOAD_FILE), + formData + ); + + console.log("Upload response:", response); + + if (!response.data?.file) { + throw new Error("Invalid upload response"); + } + + return { + id: response.data.file.id, + url: response.data.file.url, + }; + }, + onError: (error: any) => { + console.error("File upload error:", error); + toast.error(error?.message || "خطا در آپلود فایل"); + }, + }); +}; + +export const useFileDelete = () => { + return useMutation({ + mutationFn: async (fileId: string) => { + const response = await httpDeleteRequest( + APIUrlGenerator(API_ROUTES.DELETE_FILE(fileId)) + ); + return response.data; + }, + onSuccess: () => { + toast.success("فایل با موفقیت حذف شد"); + }, + onError: (error: any) => { + console.error("File delete error:", error); + toast.error(error?.message || "خطا در حذف فایل"); + }, + }); +};