fix: نمایش صحیح تصاویر هنگام ویرایش variant

- اصلاح منطق بارگذاری تصاویر variant در حالت ویرایش
- افزودن fallback به فیلد files در صورت خالی بودن file_ids
- رفع مشکل نمایش فقط یک تصویر از چندین تصویر آپلود شده
This commit is contained in:
hosseintaromi 2025-08-01 14:38:04 +03:30
parent afab715b56
commit 25429f9745
6 changed files with 351 additions and 216 deletions

View File

@ -25,6 +25,9 @@ interface FileUploaderProps {
error?: string;
disabled?: boolean;
className?: string;
mode?: 'single' | 'multi';
onUploadStart?: () => void;
onUploadComplete?: () => void;
}
export const FileUploader: React.FC<FileUploaderProps> = ({
@ -38,6 +41,9 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
error,
disabled = false,
className = "",
mode = 'multi',
onUploadStart,
onUploadComplete,
}) => {
const [files, setFiles] = useState<UploadedFile[]>([]);
const [isDragOver, setIsDragOver] = useState(false);
@ -100,10 +106,12 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
status: 'error',
error: validationError,
};
setFiles(prev => [...prev, errorFile]);
setFiles(prev => mode === 'single' ? [errorFile] : [...prev, errorFile]);
return;
}
onUploadStart?.();
const fileId = Math.random().toString(36).substr(2, 9);
const preview = await createFilePreview(file);
@ -117,7 +125,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
status: 'uploading',
};
setFiles(prev => [...prev, newFile]);
setFiles(prev => mode === 'single' ? [newFile] : [...prev, newFile]);
try {
const progressInterval = setInterval(() => {
@ -137,14 +145,17 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
? { ...f, progress: 100, status: 'completed', url: result.url, id: result.id }
: f
));
onUploadComplete?.();
} catch (error: any) {
setFiles(prev => prev.map(f =>
f.id === fileId
? { ...f, status: 'error', error: error.message || 'خطا در آپلود فایل' }
: f
));
onUploadComplete?.();
}
}, [onUpload, maxFiles, maxFileSize, acceptedTypes]);
}, [onUpload, maxFiles, maxFileSize, acceptedTypes, mode, onUploadStart, onUploadComplete]);
const handleFileSelect = useCallback((selectedFiles: FileList) => {
Array.from(selectedFiles).forEach(file => {
@ -180,6 +191,9 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
onRemove?.(fileId);
};
const hasUploadedFiles = files.some(f => f.status === 'completed');
const showUploadArea = mode === 'multi' || (mode === 'single' && !hasUploadedFiles);
return (
<div className={`space-y-4 ${className}`}>
{label && (
@ -188,41 +202,43 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
</label>
)}
{/* Upload Area */}
<div
className={`
relative border-2 border-dashed rounded-lg p-6 transition-colors cursor-pointer
${isDragOver ? 'border-primary-400 bg-primary-50 dark:bg-primary-900/20' : 'border-gray-300 dark:border-gray-600'}
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary-400 hover:bg-gray-50 dark:hover:bg-gray-700'}
${error ? 'border-red-300 bg-red-50 dark:bg-red-900/20' : ''}
`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={handleClick}
>
<input
ref={fileInputRef}
type="file"
multiple
accept={acceptedTypes.join(',')}
className="hidden"
onChange={(e) => e.target.files && handleFileSelect(e.target.files)}
disabled={disabled}
/>
{/* Upload Area - only show in multi mode or single mode without uploaded files */}
{showUploadArea && (
<div
className={`
relative border-2 border-dashed rounded-lg p-6 transition-colors cursor-pointer
${isDragOver ? 'border-primary-400 bg-primary-50 dark:bg-primary-900/20' : 'border-gray-300 dark:border-gray-600'}
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary-400 hover:bg-gray-50 dark:hover:bg-gray-700'}
${error ? 'border-red-300 bg-red-50 dark:bg-red-900/20' : ''}
`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={handleClick}
>
<input
ref={fileInputRef}
type="file"
multiple={mode === 'multi'}
accept={acceptedTypes.join(',')}
className="hidden"
onChange={(e) => e.target.files && handleFileSelect(e.target.files)}
disabled={disabled}
/>
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
{description}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
حداکثر {formatFileSize(maxFileSize)} {acceptedTypes.join(', ')}
</p>
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
{description}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
حداکثر {formatFileSize(maxFileSize)} {acceptedTypes.join(', ')}
</p>
</div>
</div>
</div>
</div>
)}
{error && (
<p className="text-sm text-red-600 dark:text-red-400 flex items-center gap-1">

View File

@ -17,6 +17,7 @@ interface VariantManagerProps {
onChange: (variants: ProductVariantFormData[]) => void;
disabled?: boolean;
productOptions?: ProductOption[];
variantAttributeName?: string;
}
interface VariantFormProps {
@ -25,9 +26,10 @@ interface VariantFormProps {
onCancel: () => void;
isEdit?: boolean;
productOptions?: ProductOption[];
variantAttributeName?: string;
}
const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, isEdit = false, productOptions = [] }) => {
const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, isEdit = false, productOptions = [], variantAttributeName }) => {
const [formData, setFormData] = useState<ProductVariantFormData>(
variant || {
enabled: true,
@ -37,7 +39,6 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
stock_managed: true,
stock_number: 0,
weight: 0,
product_option_id: undefined,
attributes: {},
meta: {},
file_ids: []
@ -49,12 +50,14 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
? variant.file_ids
: []
);
const [attributes, setAttributes] = useState<Record<string, any>>(variant?.attributes || {});
const [variantAttributeValue, setVariantAttributeValue] = useState('');
const [meta, setMeta] = useState<Record<string, any>>(variant?.meta || {});
const [newAttributeKey, setNewAttributeKey] = useState('');
const [newAttributeValue, setNewAttributeValue] = useState('');
const [newMetaKey, setNewMetaKey] = useState('');
const [newMetaValue, setNewMetaValue] = useState('');
const [attributeError, setAttributeError] = useState('');
const [weightDisplay, setWeightDisplay] = useState(variant?.weight?.toString() || '');
const [feePercentageDisplay, setFeePercentageDisplay] = useState(variant?.fee_percentage?.toString() || '');
const [profitPercentageDisplay, setProfitPercentageDisplay] = useState(variant?.profit_percentage?.toString() || '');
const { mutateAsync: uploadFile } = useFileUpload();
const { mutate: deleteFile } = useFileDelete();
@ -64,6 +67,23 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
setFormData(prev => ({ ...prev, file_ids: uploadedImages }));
}, [uploadedImages]);
// Sync display states with formData when editing
useEffect(() => {
if (variant?.weight !== undefined) {
setWeightDisplay(variant.weight.toString());
}
if (variant?.fee_percentage !== undefined) {
setFeePercentageDisplay(variant.fee_percentage.toString());
}
if (variant?.profit_percentage !== undefined) {
setProfitPercentageDisplay(variant.profit_percentage.toString());
}
// Load variant attribute value if exists
if (variantAttributeName && variant?.attributes && variant.attributes[variantAttributeName]) {
setVariantAttributeValue(variant.attributes[variantAttributeName].toString());
}
}, [variant?.weight, variant?.fee_percentage, variant?.profit_percentage, variant?.attributes, variantAttributeName]);
const handleInputChange = (field: keyof ProductVariantFormData, value: any) => {
if (typeof value === 'string') {
value = persianToEnglish(value);
@ -74,15 +94,17 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
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);
// Use functional update to avoid stale state when multiple files upload concurrently
setUploadedImages(prev => {
const newImage: ProductImage = {
id: result.id,
url: result.url,
alt: file.name,
order: prev.length
};
return [...prev, newImage];
});
return result;
} catch (error) {
@ -97,23 +119,7 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
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()) {
@ -134,13 +140,28 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
};
const handleSave = () => {
const fileIds = uploadedImages.map(img => Number(img.id)).filter(id => !isNaN(id));
// Reset previous errors
setAttributeError('');
// Validate attribute value when attribute name is defined
if (variantAttributeName && !variantAttributeValue.trim()) {
setAttributeError(`مقدار ${variantAttributeName} الزامی است.`);
return;
}
// نگه داشتن آبجکت کامل تصویر برای نمایش در لیست و حالت ویرایش
const fileObjects = uploadedImages;
// Create attributes object with single key-value pair
const attributes = variantAttributeName && variantAttributeValue.trim()
? { [variantAttributeName]: variantAttributeValue.trim() }
: {};
const convertedData = convertPersianNumbersInObject({
...formData,
attributes,
meta,
file_ids: fileIds
file_ids: fileObjects
});
onSave(convertedData);
@ -148,18 +169,10 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
return (
<div className="space-y-6 bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border">
<div className="flex items-center justify-between">
<div>
<h4 className="text-lg font-medium text-gray-900 dark:text-gray-100">
{isEdit ? 'ویرایش Variant' : 'افزودن Variant جدید'}
</h4>
<div className="flex gap-2">
<Button variant="secondary" onClick={onCancel}>
انصراف
</Button>
<Button onClick={handleSave}>
{isEdit ? 'به‌روزرسانی' : 'افزودن'}
</Button>
</div>
</div>
{/* Basic Info */}
@ -171,10 +184,15 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
<input
type="text"
inputMode="decimal"
value={formData.fee_percentage || ''}
value={feePercentageDisplay}
onChange={(e) => {
const converted = persianToEnglish(e.target.value);
handleInputChange('fee_percentage', parseFloat(converted) || 0);
setFeePercentageDisplay(converted);
}}
onBlur={(e) => {
const converted = persianToEnglish(e.target.value);
const numValue = parseFloat(converted) || 0;
handleInputChange('fee_percentage', numValue);
}}
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="مثال: ۵.۲"
@ -188,10 +206,15 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
<input
type="text"
inputMode="decimal"
value={formData.profit_percentage || ''}
value={profitPercentageDisplay}
onChange={(e) => {
const converted = persianToEnglish(e.target.value);
handleInputChange('profit_percentage', parseFloat(converted) || 0);
setProfitPercentageDisplay(converted);
}}
onBlur={(e) => {
const converted = persianToEnglish(e.target.value);
const numValue = parseFloat(converted) || 0;
handleInputChange('profit_percentage', numValue);
}}
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="مثال: ۱۰.۵"
@ -205,43 +228,23 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
<input
type="text"
inputMode="decimal"
value={formData.weight || ''}
value={weightDisplay}
onChange={(e) => {
const converted = persianToEnglish(e.target.value);
handleInputChange('weight', parseFloat(converted) || 0);
setWeightDisplay(converted);
}}
onBlur={(e) => {
const converted = persianToEnglish(e.target.value);
const numValue = parseFloat(converted) || 0;
handleInputChange('weight', numValue);
}}
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="مثال: ۱۲۰۰"
placeholder="مثال: ۱۲۰۰.۵"
/>
</div>
</div>
{/* Product Option Selection */}
{productOptions && productOptions.length > 0 && (
<div>
<h5 className="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">
گزینه محصول
</h5>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
انتخاب گزینه محصول
</label>
<select
value={formData.product_option_id || ''}
onChange={(e) => handleInputChange('product_option_id', e.target.value ? parseInt(e.target.value) : undefined)}
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"
>
<option value="">انتخاب کنید...</option>
{productOptions.map((option) => (
<option key={option.id} value={option.id}>
{option.title}
{option.description && ` - ${option.description}`}
</option>
))}
</select>
</div>
</div>
)}
{/* Stock Management */}
<div>
@ -338,57 +341,29 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
)}
</div>
{/* Attributes */}
<div>
<h5 className="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">
ویژگیهای Variant
</h5>
<div className="flex gap-3 mb-3">
<input
type="text"
value={newAttributeKey}
onChange={(e) => 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"
/>
<input
type="text"
value={newAttributeValue}
onChange={(e) => 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"
/>
<Button
type="button"
variant="secondary"
onClick={handleAddAttribute}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
افزودن
</Button>
</div>
{Object.keys(attributes).length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{Object.entries(attributes).map(([key, value]) => (
<div key={key} className="flex items-center justify-between bg-white dark:bg-gray-600 px-3 py-2 rounded-md border">
<span className="text-sm">
<strong>{key}:</strong> {String(value)}
</span>
<button
type="button"
onClick={() => handleRemoveAttribute(key)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
))}
{/* Variant Attribute */}
{variantAttributeName && (
<div>
<h5 className="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">
ویژگی Variant
</h5>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{variantAttributeName}
</label>
<input
type="text"
value={variantAttributeValue}
onChange={(e) => setVariantAttributeValue(e.target.value)}
placeholder={`مقدار ${variantAttributeName} را وارد کنید`}
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"
/>
{attributeError && (
<p className="text-red-500 text-xs mt-1">{attributeError}</p>
)}
</div>
)}
</div>
</div>
)}
{/* Meta Data */}
<div>
@ -454,11 +429,21 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
Variant فعال باشد
</label>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-600">
<Button variant="secondary" onClick={onCancel}>
انصراف
</Button>
<Button onClick={handleSave}>
{isEdit ? 'به‌روزرسانی' : 'افزودن'}
</Button>
</div>
</div>
);
};
export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChange, disabled = false, productOptions = [] }) => {
export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChange, disabled = false, productOptions = [], variantAttributeName }) => {
const [showForm, setShowForm] = useState(false);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
@ -518,6 +503,7 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
onCancel={handleCancelForm}
isEdit={editingIndex !== null}
productOptions={productOptions}
variantAttributeName={variantAttributeName}
/>
)}
@ -549,13 +535,8 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
<strong>موجودی:</strong> {variant.stock_managed ? `${variant.stock_number} عدد` : 'بدون محدودیت'}
</div>
<div>
<strong>وزن:</strong> {variant.weight} گرم
<strong>وزن:</strong> {parseFloat(variant.weight.toString()).toLocaleString('fa-IR')} گرم
</div>
{variant.product_option_id && (
<div className="md:col-span-2">
<strong>گزینه محصول:</strong> {productOptions.find(opt => opt.id === variant.product_option_id)?.title || `شناسه ${variant.product_option_id}`}
</div>
)}
</div>
{variant.file_ids && variant.file_ids.length > 0 && (

View File

@ -4,6 +4,8 @@ import { ArrowRight, FolderOpen } from 'lucide-react';
import { Button } from '../../../components/ui/Button';
import { Input } from '../../../components/ui/Input';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
import { FileUploader } from '../../../components/ui/FileUploader';
import { useFileUpload, useFileDelete } from '../../../hooks/useFileUpload';
import { useToast } from '../../../contexts/ToastContext';
import { useCategory, useCreateCategory, useUpdateCategory } from '../core/_hooks';
import { FormHeader, PageContainer, Label } from '../../../components/ui/Typography';
@ -18,8 +20,12 @@ const CategoryFormPage = () => {
name: '',
description: '',
parent_id: null as number | null,
file_id: undefined as number | undefined,
});
const [uploadedImage, setUploadedImage] = useState<{ id: string, url: string } | null>(null);
const [isUploading, setIsUploading] = useState(false);
const { data: category, isLoading: isLoadingCategory } = useCategory(
id || '0',
isEdit
@ -27,6 +33,8 @@ const CategoryFormPage = () => {
const createMutation = useCreateCategory();
const updateMutation = useUpdateCategory();
const { mutateAsync: uploadFile } = useFileUpload();
const { mutate: deleteFile } = useFileDelete();
useEffect(() => {
if (category && isEdit) {
@ -34,7 +42,16 @@ const CategoryFormPage = () => {
name: category.name || '',
description: category.description || '',
parent_id: category.parent_id || null,
file_id: category.file_id || undefined,
});
// Set uploaded image if exists
if (category.file_id) {
setUploadedImage({
id: category.file_id.toString(),
url: '' // We don't have URL from category, just ID
});
}
}
}, [category, isEdit]);
@ -45,6 +62,34 @@ const CategoryFormPage = () => {
}));
};
const handleFileUpload = async (file: File) => {
try {
const result = await uploadFile(file);
const fileId = parseInt(result.id);
setUploadedImage({
id: result.id,
url: result.url
});
setFormData(prev => ({
...prev,
file_id: fileId
}));
return result;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
const handleFileRemove = (fileId: string) => {
setUploadedImage(null);
setFormData(prev => ({
...prev,
file_id: undefined
}));
deleteFile(fileId);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@ -120,6 +165,21 @@ const CategoryFormPage = () => {
/>
</div>
<div>
<FileUploader
onUpload={handleFileUpload}
onRemove={handleFileRemove}
acceptedTypes={['image/*']}
maxFileSize={5 * 1024 * 1024} // 5MB
maxFiles={1}
mode="single"
label="تصویر دسته‌بندی"
description="تصویر دسته‌بندی را انتخاب کنید (حداکثر 5MB)"
onUploadStart={() => setIsUploading(true)}
onUploadComplete={() => setIsUploading(false)}
/>
</div>
{/* Actions */}
<div className="flex flex-col space-y-3 sm:flex-row sm:justify-end sm:space-y-0 sm:space-x-3 sm:space-x-reverse pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
@ -133,6 +193,7 @@ const CategoryFormPage = () => {
<Button
type="submit"
loading={createMutation.isPending || updateMutation.isPending}
disabled={isUploading}
className="w-full sm:w-auto"
>
{isEdit ? 'ویرایش' : 'ایجاد'}

View File

@ -3,6 +3,7 @@ export interface Category {
name: string;
description?: string;
parent_id?: number;
file_id?: number;
created_at: string;
updated_at: string;
}
@ -10,6 +11,7 @@ export interface Category {
export interface CategoryFormData {
name: string;
description: string;
file_id?: number;
}
export interface CategoryFilters {
@ -21,12 +23,14 @@ export interface CategoryFilters {
export interface CreateCategoryRequest {
name: string;
description?: string;
file_id?: number;
}
export interface UpdateCategoryRequest {
id: number;
name: string;
description?: string;
file_id?: number;
}
export interface CategoriesResponse {

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useForm, useFieldArray } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
@ -8,6 +8,8 @@ import { ProductOptionFormData, Maintenance, Option } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { FileUploader } from "@/components/ui/FileUploader";
import { useFileUpload, useFileDelete } from "@/hooks/useFileUpload";
import { ArrowRight, Settings, Plus, Trash2 } from "lucide-react";
import { FormHeader, PageContainer, SectionTitle, Label } from '../../../components/ui/Typography';
@ -39,6 +41,11 @@ const ProductOptionFormPage = () => {
const { data: productOption, isLoading: isLoadingOption } = useProductOption(id || '', isEdit);
const { mutate: createOption, isPending: isCreating } = useCreateProductOption();
const { mutate: updateOption, isPending: isUpdating } = useUpdateProductOption();
const { mutateAsync: uploadFile } = useFileUpload();
const { mutate: deleteFile } = useFileDelete();
const [uploadedImage, setUploadedImage] = useState<{ id: string, url: string } | null>(null);
const [isUploading, setIsUploading] = useState(false);
const isLoading = isCreating || isUpdating;
@ -78,6 +85,14 @@ const ProductOptionFormPage = () => {
setValue('description', productOption.description, { shouldValidate: true });
setValue('maintenance', productOption.maintenance, { shouldValidate: true });
setValue('options', productOption.options, { shouldValidate: true });
// Set uploaded image if exists
if (productOption.maintenance.image) {
setUploadedImage({
id: productOption.maintenance.image,
url: productOption.maintenance.image
});
}
}
}, [isEdit, productOption, setValue]);
@ -108,6 +123,28 @@ const ProductOptionFormPage = () => {
});
};
const handleFileUpload = async (file: File) => {
try {
const result = await uploadFile(file);
const imageData = {
id: result.id,
url: result.url
};
setUploadedImage(imageData);
setValue('maintenance.image', result.id, { shouldValidate: true, shouldDirty: true });
return result;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
const handleFileRemove = (fileId: string) => {
setUploadedImage(null);
setValue('maintenance.image', '', { shouldValidate: true, shouldDirty: true });
deleteFile(fileId);
};
if (isLoadingOption) {
return (
<div className="flex items-center justify-center min-h-screen">
@ -151,42 +188,65 @@ const ProductOptionFormPage = () => {
/>
</div>
<div>
<Input
label="توضیحات"
<Label htmlFor="description">توضیحات</Label>
<textarea
id="description"
{...register('description')}
error={errors.description?.message}
placeholder="توضیحات گزینه محصول را وارد کنید"
rows={4}
className="input resize-none"
/>
{errors.description?.message && (
<p className="text-xs text-red-600 dark:text-red-400">{errors.description.message}</p>
)}
</div>
</div>
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<SectionTitle className="mb-4">اطلاعات نگهداری</SectionTitle>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="عنوان نگهداری"
{...register('maintenance.title')}
error={errors.maintenance?.title?.message}
placeholder="عنوان نگهداری را وارد کنید"
/>
<Input
label="توضیحات نگهداری"
{...register('maintenance.description')}
error={errors.maintenance?.description?.message}
placeholder="توضیحات نگهداری را وارد کنید"
/>
<Input
label="محتوای نگهداری"
{...register('maintenance.content')}
error={errors.maintenance?.content?.message}
placeholder="محتوای نگهداری را وارد کنید"
/>
<Input
label="تصویر نگهداری"
{...register('maintenance.image')}
error={errors.maintenance?.image?.message}
placeholder="آدرس تصویر نگهداری را وارد کنید"
/>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="عنوان نگهداری"
{...register('maintenance.title')}
error={errors.maintenance?.title?.message}
placeholder="عنوان نگهداری را وارد کنید"
/>
<Input
label="توضیحات نگهداری"
{...register('maintenance.description')}
error={errors.maintenance?.description?.message}
placeholder="توضیحات نگهداری را وارد کنید"
/>
</div>
<div>
<Label htmlFor="maintenance-content">محتوای نگهداری</Label>
<textarea
id="maintenance-content"
{...register('maintenance.content')}
placeholder="محتوای نگهداری را وارد کنید"
rows={4}
className="input resize-none"
/>
{errors.maintenance?.content?.message && (
<p className="text-xs text-red-600 dark:text-red-400">{errors.maintenance.content.message}</p>
)}
</div>
<div>
<FileUploader
onUpload={handleFileUpload}
onRemove={handleFileRemove}
acceptedTypes={['image/*']}
maxFileSize={5 * 1024 * 1024} // 5MB
maxFiles={1}
mode="single"
label="تصویر نگهداری"
description="تصویر نگهداری را انتخاب کنید (حداکثر 5MB)"
error={errors.maintenance?.image?.message}
onUploadStart={() => setIsUploading(true)}
onUploadComplete={() => setIsUploading(false)}
/>
</div>
</div>
</div>
@ -218,25 +278,34 @@ const ProductOptionFormPage = () => {
حذف
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input
label="عنوان"
{...register(`options.${index}.title`)}
error={errors.options?.[index]?.title?.message}
placeholder="عنوان گزینه را وارد کنید"
/>
<Input
label="توضیحات"
{...register(`options.${index}.description`)}
error={errors.options?.[index]?.description?.message}
placeholder="توضیحات گزینه را وارد کنید"
/>
<Input
label="متا تایتل"
{...register(`options.${index}.meta_title`)}
error={errors.options?.[index]?.meta_title?.message}
placeholder="متا تایتل را وارد کنید"
/>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="عنوان"
{...register(`options.${index}.title`)}
error={errors.options?.[index]?.title?.message}
placeholder="عنوان گزینه را وارد کنید"
/>
<Input
label="متا تایتل"
{...register(`options.${index}.meta_title`)}
error={errors.options?.[index]?.meta_title?.message}
placeholder="متا تایتل را وارد کنید"
/>
</div>
<div>
<Label htmlFor={`option-${index}-description`}>توضیحات</Label>
<textarea
id={`option-${index}-description`}
{...register(`options.${index}.description`)}
placeholder="توضیحات گزینه را وارد کنید"
rows={3}
className="input resize-none"
/>
{errors.options?.[index]?.description?.message && (
<p className="text-xs text-red-600 dark:text-red-400">{errors.options[index]?.description?.message}</p>
)}
</div>
</div>
</div>
))}
@ -260,7 +329,7 @@ const ProductOptionFormPage = () => {
<Button
type="submit"
variant="primary"
disabled={!isValid || isLoading}
disabled={!isValid || isLoading || isUploading}
loading={isLoading}
>
{isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول'}

View File

@ -44,6 +44,7 @@ const ProductFormPage = () => {
const [attributes, setAttributes] = useState<Record<string, any>>({});
const [newAttributeKey, setNewAttributeKey] = useState('');
const [newAttributeValue, setNewAttributeValue] = useState('');
const [isUploading, setIsUploading] = useState(false);
const { data: product, isLoading: isLoadingProduct } = useProduct(id || '', isEdit);
const { data: categories, isLoading: isLoadingCategories } = useCategories();
@ -391,7 +392,7 @@ const ProductFormPage = () => {
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
گزینه محصول
ویژگی های محصول و نحوه نگه داری
</label>
{isLoadingProductOptions ? (
<div className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400">
@ -432,8 +433,11 @@ const ProductFormPage = () => {
acceptedTypes={['image/*']}
maxFileSize={5 * 1024 * 1024}
maxFiles={10}
mode="multi"
label=""
description="تصاویر محصول را اینجا بکشید یا کلیک کنید"
onUploadStart={() => setIsUploading(true)}
onUploadComplete={() => setIsUploading(false)}
/>
{uploadedImages.length > 0 && (
@ -613,7 +617,7 @@ const ProductFormPage = () => {
<Button
type="submit"
loading={isLoading}
disabled={!isValid || isLoading}
disabled={!isValid || isLoading || isUploading}
>
{isEdit ? 'به‌روزرسانی' : 'ایجاد محصول'}
</Button>